Create: Computing AgesDoc Brass
Recettes : vrais projets

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 :

champdans la requêtedans la réponse
secretle mot secret partagéle même mot
idun nombre, augmenté de un à chaque nouvel ordrel'id de la requête à laquelle elle répond
ordre"demarrer", "arreter" ou "etat"
oktrue, ou false avec erreur
en_marche, objetsl'é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.send renvoie true quand 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 que net.send renvoie true. 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.send renvoie false tout 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'id qu'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 id reç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.

Brass
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

mettre_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.)

Brass
local function mettre_en_marche(oui)
  en_marche = oui
  rs.set(EMBRAYAGE, not oui)
  fs.write(FICHIER_ETAT, tostring(oui))
end

etat 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.

Brass
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

startup (machine)
-- 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())
end

Son écran est un journal, une ligne par ordre, pratique pour voir qui pilote la chaîne :

Écran
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).

Brass
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

L'é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.

Brass
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.

Brass
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 »
end

sur_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.

Brass
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

La 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

startup (télécommande)
-- 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.

démo de la télécommande
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()
Écran
Écran

La tester

  1. Sur l'ordinateur de la machine, tapez id et notez le numéro ; mettez-le dans MACHINE sur la télécommande. Lancez d'abord l'ordinateur de la machine (startup), puis la télécommande.
  2. 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.
  3. 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.
  4. Hors de portée. Cassez le Modem radio de la machine : la télécommande affiche « non remis » tout de suite, à chaque essai.
  5. Le secret. Changez une lettre de SECRET sur la télécommande : la machine note message refusé de n°5 et la télécommande ne reçoit pas de réponse.
  6. 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.send livre à 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 un net.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 end au début de traiter.

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 MACHINE une liste, ajoutez au menu une ligne pour choisir la cible, et gardez un en_attente par 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) dans mettre_en_marche (voir link).
  • 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_speed cô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.