Télécommande par le réseau
Démarrer, arrêter et surveiller une chaîne de production depuis un autre ordinateur, avec des ordres numérotés, des réponses, des délais, des renvois et un mot secret partagé.
Votre chaîne de Presses mécaniques tourne à l'autre bout de la base, et vous voulez la démarrer, l'arrêter et voir ce qu'elle a produit depuis la salle de contrôle, la gare ou le dirigeable. Cette recette utilise deux ordinateurs : une télécommande avec un menu, et un ordinateur de machine à côté de la chaîne, qui exécute les ordres et répond.
Envoyer un message tient en une ligne (net.send). Le travail est dans tout ce qui peut mal tourner en route : l'ordinateur de la machine redémarre, son chunk n'est pas chargé, une réponse arrive en retard, quelqu'un d'autre envoie des ordres. Le petit protocole ci-dessous gère tout cela, et vous pourrez le réutiliser pour toute paire d'ordinateurs qui doivent se parler de façon fiable : portes, gares, ascenseurs, flotte de dirigeables.
Ce qu'il vous faut
- Deux ordinateurs qui peuvent se parler. Un Mini-ordinateur ou plus récent pour un Câble de données entre eux ; un Micro-ordinateur, un Microcontrôleur ou un Ordinateur moderne pour la radio. Voir
Réseaux. - Une liaison entre eux : un Modem radio contre chaque ordinateur (128 blocs), des Routeurs Wi-Fi (256 blocs, et les routeurs de même SSID se joignent n'importe où), ou tout simplement un Câble de données de l'un à l'autre.
- Sur la chaîne : un embrayage de Create sur l'arbre qui entraîne la chaîne, contre la face gauche de l'ordinateur de la machine (un embrayage alimenté débraye : la chaîne s'arrête), et le coffre que remplit la chaîne contre sa face droite. La gauche et la droite se lisent de face, en regardant l'écran. Le coffre est facultatif : sans lui, l'état le signale.
- De la rotation pour les deux ordinateurs, comme toujours.
Ordinateur de la machine, vu de dessus :
arbre du moteur ==[Embrayage][M][Coffre] M ordinateur de la machine (Modem radio dessus)
|| l'embrayage à sa gauche, le coffre à sa droite
[chaîne de Presses mécaniques]
Télécommande : n'importe où à portée radio (ou sur le même câble), avec un modem dessus.Le protocole
Les deux ordinateurs échangent des tables sur le canal "chaine". Une requête va de la télécommande à la machine, la réponse revient à la télécommande :
| champ | dans la requête | dans la réponse |
|---|---|---|
secret | le mot secret partagé | le même mot |
id | un nombre, augmenté de un à chaque nouvel ordre | l'id de la requête à laquelle elle répond |
ordre | "demarrer", "arreter" ou "etat" | |
ok | true, ou false avec erreur | |
en_marche, objets | l'état de la chaîne, les objets du coffre de sortie |
Pourquoi chaque champ est là :
- La réponse sert d'accusé de réception.
net.sendrenvoietruequand le message a été déposé dans la file de l'autre ordinateur, pas quand l'ordre a été exécuté. Un ordinateur de machine dont le programme s'est arrêté, revenu à l'invite du shell, perd même le message alors quenet.sendrenvoietrue. Seule une réponse prouve que la machine a lu l'ordre et l'a exécuté. - Un délai et des renvois. Sans réponse au bout de deux secondes, la télécommande renvoie la même requête, trois fois au plus, puis vous dit que la machine ne répond pas.
net.sendrenvoiefalsetout de suite quand l'autre ordinateur est éteint, hors de portée ou dans un chunk non chargé : cet essai compte aussi. - L'id relie une réponse à sa requête. La réponse tardive d'un ancien ordre ne doit pas passer pour celle du nouveau. La télécommande n'accepte qu'une réponse portant l'
idqu'elle attend. - L'id empêche aussi d'exécuter un ordre deux fois. Quand l'ordre est arrivé mais que sa réponse s'est perdue, la télécommande le renvoie. La machine retient la dernière requête de chaque télécommande : le même
idreçoit la même réponse, et l'ordre n'est pas exécuté une seconde fois. Avec « démarrer » et « arrêter », une double exécution serait sans gravité, mais pas avec un ordre comme « livrer une caisse ». - Le mot secret empêche les ordinateurs des autres joueurs de piloter votre chaîne. La machine ignore toute requête qui ne le contient pas.
télécommande machine
| --- {id=41, ordre="demarrer"} --------> | machine en plein redémarrage : perdu
| 2 s, pas de réponse |
| --- {id=41, ordre="demarrer"} --------> | l'exécute, retient 41
| <-------------- {id=41, ok=true} ------- |
| |
| --- {id=42, ordre="arreter"} ---------> | l'exécute, retient 42
| X <---- {id=42, ok=true} ------- | réponse perdue
| --- {id=42, ordre="arreter"} ---------> | encore 42 : même réponse, pas de second arrêt
| <-------------- {id=42, ok=true} ------- |L'ordinateur de la machine
Comment ça marche
Les réglages d'abord. Le mot secret doit être le même sur les deux ordinateurs ; choisissez plus long qu'un mot de passe qu'on devinerait.
local SECRET = "bouilloire-cuivre-42" -- le même mot sur les deux ordinateurs
local CANAL = "chaine"
local EMBRAYAGE = "left" -- un embrayage alimenté arrête la chaîne
local SORTIE = "right" -- le coffre que remplit la chaîne (facultatif)
local FICHIER_ETAT = "etat_chaine" -- marche ou arrêt, gardé d'un redémarrage à l'autremettre_en_marche commande la chaîne et écrit son état dans un fichier. Au démarrage, le programme le relit : après un redémarrage du serveur, la chaîne retrouve son état. (Un redémarrage relâche un instant toutes les sorties de redstone, comme une vraie remise à zéro : l'embrayage lâche jusqu'à ce que le programme le remette, une fraction de seconde plus tard.)
local function mettre_en_marche(oui)
en_marche = oui
rs.set(EMBRAYAGE, not oui)
fs.write(FICHIER_ETAT, tostring(oui))
endetat construit la réponse : l'état de la chaîne, et le nombre d'objets du coffre de sortie s'il y en a un. Le coffre est lu à travers pcall : un coffre cassé donne une réponse sans objets au lieu d'arrêter le programme.
executer exécute un ordre et renvoie la réponse. Un ordre inconnu reçoit ok = false et une erreur, pour que la télécommande puisse dire pourquoi rien ne s'est passé.
traiter est le cœur du programme. Il vérifie le canal, la forme du message et le secret, puis regarde la dernière requête de cet expéditeur : même id, même réponse, rien n'est refait. Sinon il exécute l'ordre, ajoute l'id et le secret à la réponse, la retient, et la renvoie avec net.send à m.sender.
local deja = dernier[m.sender]
local reponse
if deja and deja.id == d.id then
reponse = deja.reponse -- un renvoi : la même réponse, rien n'est fait deux fois
else
reponse = executer(d.ordre)
reponse.id = d.id
reponse.secret = SECRET
dernier[m.sender] = {id = d.id, reponse = reponse}
end
net.send(m.sender, reponse, CANAL)La boucle principale tient en une ligne : traiter(net.receive()). net.receive attend le prochain message ; cet ordinateur n'a rien d'autre à attendre.
Le programme complet
-- Chaîne télécommandée : exécute les ordres de la télécommande et répond.
-- Micro-ordinateur avec un Modem radio (ou n'importe quel ordinateur sur un Câble de données),
-- un embrayage de Create à gauche, le coffre que remplit la chaîne à droite.
local SECRET = "bouilloire-cuivre-42" -- le même mot sur les deux ordinateurs
local CANAL = "chaine"
local EMBRAYAGE = "left" -- un embrayage alimenté arrête la chaîne
local SORTIE = "right" -- le coffre que remplit la chaîne (facultatif)
local FICHIER_ETAT = "etat_chaine" -- marche ou arrêt, gardé d'un redémarrage à l'autre
local en_marche = false
local dernier = {} -- dernière réponse envoyée à chaque télécommande : dernier[expediteur] = {id =, reponse =}
local function heure()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function mettre_en_marche(oui)
en_marche = oui
rs.set(EMBRAYAGE, not oui)
fs.write(FICHIER_ETAT, tostring(oui))
end
-- Ce que la télécommande veut savoir : en marche ou non, et les objets produits
local function etat()
local reponse = {ok = true, en_marche = en_marche}
local coffre = peripheral.wrap(SORTIE)
if coffre then
local r = pcall(coffre.count)
if r.ok then reponse.objets = r.value end
end
return reponse
end
-- Exécute un ordre et renvoie la réponse
local function executer(ordre)
if ordre == "demarrer" then
mettre_en_marche(true)
elseif ordre == "arreter" then
mettre_en_marche(false)
elseif ordre ~= "etat" then
return {ok = false, erreur = "ordre inconnu"}
end
return etat()
end
-- Une requête : vérifiée, exécutée une seule fois, avec une réponse
local function traiter(m)
local d = m.data
if m.channel ~= CANAL then return end
if type(d) ~= "table" or d.secret ~= SECRET or type(d.id) ~= "number" then
print(heure() .. " message refusé de n°" .. m.sender)
return
end
local deja = dernier[m.sender]
local reponse
if deja and deja.id == d.id then
reponse = deja.reponse -- un renvoi : la même réponse, rien n'est fait deux fois
else
reponse = executer(d.ordre)
reponse.id = d.id
reponse.secret = SECRET
dernier[m.sender] = {id = d.id, reponse = reponse}
print(heure() .. " n°" .. m.sender .. " " .. tostring(d.ordre) .. " : "
.. (reponse.ok and "fait" or reponse.erreur))
end
net.send(m.sender, reponse, CANAL)
end
mettre_en_marche(fs.read(FICHIER_ETAT) == "true")
print("Chaîne n°" .. net.id() .. " prête, " .. (en_marche and "en marche" or "arrêtée"))
while true do
traiter(net.receive())
endSon écran est un journal, une ligne par ordre, pratique pour voir qui pilote la chaîne :
Chaîne n°12 prête, arrêtée 18:02 n°5 etat : fait 18:02 n°5 demarrer : fait 18:40 message refusé de n°31 18:41 n°5 arreter : fait
La télécommande
Comment ça marche
Les réglages désignent la machine par son numéro : tapez id dans le shell de l'ordinateur de la machine pour le lire (ou voyez les variantes pour la trouver par son étiquette).
local SECRET = "bouilloire-cuivre-42"
local CANAL = "chaine"
local MACHINE = 12 -- numéro de l'ordinateur de la chaîne : tapez "id" dessus
local DELAI = 2 -- secondes d'attente d'une réponse
local ESSAIS = 3 -- envois avant d'abandonnerL'état du programme tient en quelques variables de niveau fichier. en_attente est l'ordre qui attend sa réponse, ou nil : un seul ordre à la fois garde les choses simples, et une touche pressée entre-temps affiche seulement « Occupé ». Le premier id est tiré au hasard : si les numéros repartaient de 1 à chaque démarrage, le premier ordre après un redémarrage porterait l'id d'un ancien, et la machine le prendrait pour un renvoi et répondrait sans rien faire.
local prochain_id = math.random(1, 1000000) -- pas 1 à nouveau après un redémarrage
local en_attente = nil -- l'ordre qui attend sa réponse : {id =, ordre =, essais =, minuterie =}transmettre envoie la requête en attente et lance son délai avec os.start_timer. Elle sert au premier essai et à chaque renvoi, avec le même id. commander refuse un nouvel ordre tant qu'un autre attend, sinon le numérote et l'envoie.
local function transmettre()
en_attente.essais = en_attente.essais + 1
local requete = {secret = SECRET, id = en_attente.id, ordre = en_attente.ordre}
local remis = net.send(MACHINE, requete, CANAL)
en_attente.minuterie = os.start_timer(DELAI)
-- ... une note à l'écran, « envoyé » ou « non remis »
endsur_message n'accepte une réponse que si un ordre attend et que la réponse vient de la machine, sur le canal, avec le secret et l'id attendu. Tout le reste (la réponse tardive d'un ordre plus ancien, un inconnu) est ignoré.
sur_minuterie fait de même avec les minuteries : celle d'une requête qui a déjà reçu sa réponse court toujours, et son évènement arrive plus tard. Seule compte la minuterie de l'essai en cours ; alors la requête est renvoyée, ou abandonnée après ESSAIS essais.
local function sur_minuterie(id)
if en_attente == nil or id ~= en_attente.minuterie then return end -- une vieille minuterie
if en_attente.essais < ESSAIS then
transmettre()
else
dire(en_attente.ordre .. " : pas de réponse après " .. ESSAIS .. " essais", term.colors.red)
chaine.etat = "INCONNU"
en_attente = nil
end
endLa boucle principale lit tous les évènements avec os.pull_event : les touches (l'évènement char des chiffres), les clics sur le menu, les messages et les minuteries. Elle ne doit pas attendre avec net.receive : cet appel n'attend que des messages et jette les touches et les minuteries qui arrivent entre-temps, si bien que le menu et les délais ne marcheraient plus.
Le menu est dessiné avec term : chaque chiffre sur fond gris clair, l'état de la chaîne en vert ou en rouge. L'écran est redessiné après chaque évènement, ce qui coûte peu : ce n'est que du texte.
Le programme complet
-- Télécommande de la chaîne : un menu, des ordres numérotés avec accusé de réception,
-- renvoyés quand la réponse ne vient pas. Micro-ordinateur avec un Modem radio.
local SECRET = "bouilloire-cuivre-42"
local CANAL = "chaine"
local MACHINE = 12 -- numéro de l'ordinateur de la chaîne : tapez "id" dessus
local DELAI = 2 -- secondes d'attente d'une réponse
local ESSAIS = 3 -- envois avant d'abandonner
local MENU = {
{touche = "1", ordre = "demarrer", texte = "Démarrer la chaîne"},
{touche = "2", ordre = "arreter", texte = "Arrêter la chaîne"},
{touche = "3", ordre = "etat", texte = "Demander l'état"},
}
local W = term.get_size().w
local prochain_id = math.random(1, 1000000) -- pas 1 à nouveau après un redémarrage
local en_attente = nil -- l'ordre qui attend sa réponse : {id =, ordre =, essais =, minuterie =}
local chaine = {etat = "INCONNU", objets = nil, releve = "--:--"}
local note, couleur_note = "", term.colors.light_gray
local function heure()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function dire(texte, couleur)
note, couleur_note = texte, couleur
end
-- 1. L'écran
local function ecrire_en(x, y, texte, fg, bg)
term.set_cursor(x, y)
term.set_fg(fg)
term.set_bg(bg or term.colors.black)
term.write(texte)
end
local function dessiner()
term.set_bg(term.colors.black)
term.clear()
ecrire_en(2, 1, "TÉLÉCOMMANDE", term.colors.yellow)
local cible = "chaîne n°" .. MACHINE
ecrire_en(W - #cible, 1, cible, term.colors.gray)
ecrire_en(1, 2, string.rep("-", W), term.colors.gray)
for i, choix in ipairs(MENU) do
ecrire_en(3, 2 + i * 2, " " .. choix.touche .. " ", term.colors.black, term.colors.light_gray)
ecrire_en(7, 2 + i * 2, choix.texte, term.colors.white)
end
local couleur_etat = term.colors.gray
if chaine.etat == "EN MARCHE" then couleur_etat = term.colors.lime end
if chaine.etat == "ARRÊTÉE" then couleur_etat = term.colors.red end
ecrire_en(3, 11, "Chaîne", term.colors.light_gray)
ecrire_en(13, 11, chaine.etat, couleur_etat)
ecrire_en(3, 12, "Sortie", term.colors.light_gray)
ecrire_en(13, 12, chaine.objets and (chaine.objets .. " objets") or "pas de coffre", term.colors.white)
ecrire_en(3, 13, "Relevé", term.colors.light_gray)
ecrire_en(13, 13, chaine.releve, term.colors.white)
ecrire_en(1, 15, string.rep("-", W), term.colors.gray)
ecrire_en(2, 16, note, couleur_note)
end
-- 2. L'envoi : chaque essai lance un nouveau délai
local function transmettre()
en_attente.essais = en_attente.essais + 1
local requete = {secret = SECRET, id = en_attente.id, ordre = en_attente.ordre}
local remis = net.send(MACHINE, requete, CANAL)
en_attente.minuterie = os.start_timer(DELAI)
local essai = " (essai " .. en_attente.essais .. "/" .. ESSAIS .. ")"
if remis then
dire(en_attente.ordre .. " : envoyé, en attente" .. essai, term.colors.yellow)
else
dire(en_attente.ordre .. " : non remis" .. essai, term.colors.orange)
end
end
local function commander(ordre)
if en_attente then
dire("Occupé : j'attends encore " .. en_attente.ordre, term.colors.orange)
return
end
prochain_id = prochain_id + 1
en_attente = {id = prochain_id, ordre = ordre, essais = 0}
transmettre()
end
-- 3. Les réponses et les délais
local function sur_message(m)
local d = m.data
if en_attente == nil or m.sender ~= MACHINE or m.channel ~= CANAL then return end
if type(d) ~= "table" or d.secret ~= SECRET or d.id ~= en_attente.id then return end
if d.ok then
chaine.etat = d.en_marche and "EN MARCHE" or "ARRÊTÉE"
chaine.objets = d.objets
chaine.releve = heure()
dire(en_attente.ordre .. " : fait", term.colors.lime)
else
dire(en_attente.ordre .. " : refusé, " .. tostring(d.erreur), term.colors.red)
end
en_attente = nil
end
local function sur_minuterie(id)
if en_attente == nil or id ~= en_attente.minuterie then return end -- une vieille minuterie
if en_attente.essais < ESSAIS then
transmettre()
else
dire(en_attente.ordre .. " : pas de réponse après " .. ESSAIS .. " essais", term.colors.red)
chaine.etat = "INCONNU"
en_attente = nil
end
end
-- 4. Boucle principale : touches, clics, messages et minuteries passent tous par os.pull_event
commander("etat")
dessiner()
while true do
local e = os.pull_event()
if e.name == "char" then
for _, choix in ipairs(MENU) do
if e.char == choix.touche then commander(choix.ordre) end
end
elseif e.name == "click" then
for i, choix in ipairs(MENU) do
if e.y == 2 + i * 2 and e.x >= 3 and e.x <= 6 + #choix.texte then commander(choix.ordre) end
end
elseif e.name == "message" then
sur_message(e)
elseif e.name == "timer" then
sur_minuterie(e.id)
end
dessiner()
endÀ quoi ça ressemble
Le même code de dessin, avec un état écrit à la main : la chaîne tourne, et un ordre « arreter » attend sa réponse après un premier essai resté sans réponse.
local MACHINE, ESSAIS = 12, 3
local MENU = {
{touche = "1", ordre = "demarrer", texte = "Démarrer la chaîne"},
{touche = "2", ordre = "arreter", texte = "Arrêter la chaîne"},
{touche = "3", ordre = "etat", texte = "Demander l'état"},
}
local W = term.get_size().w
local chaine = {etat = "EN MARCHE", objets = 1234, releve = "18:02"}
local note, couleur_note = "arreter : envoyé, en attente (essai 2/3)", term.colors.yellow
local function ecrire_en(x, y, texte, fg, bg)
term.set_cursor(x, y)
term.set_fg(fg)
term.set_bg(bg or term.colors.black)
term.write(texte)
end
local function dessiner()
term.set_bg(term.colors.black)
term.clear()
ecrire_en(2, 1, "TÉLÉCOMMANDE", term.colors.yellow)
local cible = "chaîne n°" .. MACHINE
ecrire_en(W - #cible, 1, cible, term.colors.gray)
ecrire_en(1, 2, string.rep("-", W), term.colors.gray)
for i, choix in ipairs(MENU) do
ecrire_en(3, 2 + i * 2, " " .. choix.touche .. " ", term.colors.black, term.colors.light_gray)
ecrire_en(7, 2 + i * 2, choix.texte, term.colors.white)
end
local couleur_etat = term.colors.gray
if chaine.etat == "EN MARCHE" then couleur_etat = term.colors.lime end
if chaine.etat == "ARRÊTÉE" then couleur_etat = term.colors.red end
ecrire_en(3, 11, "Chaîne", term.colors.light_gray)
ecrire_en(13, 11, chaine.etat, couleur_etat)
ecrire_en(3, 12, "Sortie", term.colors.light_gray)
ecrire_en(13, 12, chaine.objets and (chaine.objets .. " objets") or "pas de coffre", term.colors.white)
ecrire_en(3, 13, "Relevé", term.colors.light_gray)
ecrire_en(13, 13, chaine.releve, term.colors.white)
ecrire_en(1, 15, string.rep("-", W), term.colors.gray)
ecrire_en(2, 16, note, couleur_note)
end
dessiner()
La tester
- Sur l'ordinateur de la machine, tapez
idet notez le numéro ; mettez-le dansMACHINEsur la télécommande. Lancez d'abord l'ordinateur de la machine (startup), puis la télécommande. - La télécommande demande l'état d'elle-même au lancement : la chaîne s'affiche ARRÊTÉE, avec le compte du coffre. Appuyez sur 1 : la chaîne de presses se met à tourner, la télécommande affiche EN MARCHE, la machine note
demarrer : fait. - Les renvois. Privez l'ordinateur de la machine de rotation (cassez son arbre) : il se fige, et les messages attendent dans sa file. Appuyez sur 2 sur la télécommande : « essai 2/3 », « essai 3/3 », puis « pas de réponse après 3 essais ». Rendez-lui la rotation : la machine se réveille, exécute l'arrêt une seule fois (son journal n'en montre qu'une ligne) et répond à chacune des trois copies ; la télécommande n'attend plus et les ignore. Appuyez sur 3 pour lire l'état réel.
- Hors de portée. Cassez le Modem radio de la machine : la télécommande affiche « non remis » tout de suite, à chaque essai.
- Le secret. Changez une lettre de
SECRETsur la télécommande : la machine notemessage refusé de n°5et la télécommande ne reçoit pas de réponse. - Redémarrez le serveur (ou l'ordinateur de la machine avec Ctrl+R) pendant que la chaîne tourne : elle repart après le démarrage, grâce au fichier
etat_chaine.
À propos de sécurité
net.sendlivre à un seul ordinateur : les autres à portée ne voient jamais le message, et le mot secret reste entre les deux. Ne mettez jamais le secret dans unnet.broadcast: tous les ordinateurs à portée le liraient.- La machine refuse les requêtes sans le secret, et l'écrit sur son écran : vous voyez si quelqu'un essaie.
- Le secret est écrit dans les fichiers des deux ordinateurs : un joueur qui peut ouvrir vos ordinateurs peut le lire. Sur un serveur, gardez-les dans une zone protégée.
- Pour aller plus loin, la machine peut aussi n'accepter que les télécommandes connues :
if m.sender ~= 5 then return endau début detraiter.
Variantes
Trouver la machine par son étiquette. Donnez un nom à l'ordinateur de la machine avec
label chaine-presses, puis remplacez le numéro fixe sur la télécommande :Brass for _, c in ipairs(net.computers()) do if c.label == "chaine-presses" then MACHINE = c.id end end- Plusieurs chaînes. Faites de
MACHINEune liste, ajoutez au menu une ligne pour choisir la cible, et gardez unen_attentepar machine. - Un état automatique. Une seconde minuterie, toutes les 30 secondes, appelle
commander("etat")quand rien n'attend : l'écran reste à jour sans toucher au clavier. - Une chaîne plus lointaine. Mettez l'embrayage sur une Liaison de redstone au lieu de la face de l'ordinateur :
link.set("minecraft:iron_ingot", "minecraft:redstone", not oui)dansmettre_en_marche(voirlink). - Des ordres avec une valeur. Ajoutez un champ à la requête, comme
{ordre = "vitesse", tr_min = 64}, et pilotez un Contrôleur de vitesse de rotation avec@kinetic.set_speedcôté machine. - Mettre à jour beaucoup d'ordinateurs. Les mêmes idées (une réponse par message, un délai, des renvois) envoient des programmes entiers à une flotte d'ordinateurs dans
Un projet en plusieurs fichiers.