Create: Computing AgesDoc Brass
Recettes : vrais projets

Un projet en plusieurs fichiers

Découper un gros programme en un fichier principal et des bibliothèques avec import, lire un fichier de réglages avec des valeurs par défaut, tenir un journal, et mettre le programme à jour sur beaucoup d'ordinateurs par disquette ou par le réseau.

Passé une centaine de lignes, un programme d'un seul fichier devient difficile à parcourir, et les mêmes aides (dessiner un bouton, lire un réglage, écrire un journal) finissent copiées d'un programme à l'autre. Cette recette construit un petit panneau de commande comme on construit les gros programmes : un startup court, un fichier principal, et des bibliothèques dans leur propre dossier, chargées avec import. Puis elle répond à la question qui vient avec le succès : comment installer une nouvelle version sur les douze ordinateurs qui le font tourner.

Le panneau lui-même est volontairement simple : un bouton par sortie de redstone, nommée dans un fichier de réglages. C'est la structure qui compte ; elle convient à toutes les recettes de ce livre.

Le projet

/startup                 une ligne : import "panneau/main"
/panneau/main            le programme : réglages, boutons, boucle principale
/panneau/lib/config      lit un fichier de réglages "clé = valeur", avec des valeurs par défaut
/panneau/lib/ui          aides au dessin : une barre de titre, des boutons, une ligne d'état
/panneau/lib/log         un journal de lignes datées, gardé petit
/panneau/lib/update      installe une nouvelle version reçue par le réseau
/panneau.cfg             les réglages de CET ordinateur (hors du programme)
/panneau.log             ce qui s'est passé, écrit par lib/log

Deux choix rendent tout le reste facile :

  • Le programme vit dans son propre dossier. Le copier, le mettre à jour ou le supprimer, c'est un seul dossier ; d'autres programmes peuvent vivre à côté.
  • Les réglages vivent en dehors. Chaque ordinateur garde son titre et ses sorties dans /panneau.cfg, et une nouvelle version de /panneau peut remplacer tous les fichiers du programme sans y toucher.

Un Micro-ordinateur lui va bien (couleurs, disquette, port réseau pour les mises à jour). Tapez edit panneau pour ouvrir tout le dossier dans l'éditeur, avec son arborescence à gauche : chaque fichier est à un clic.

Comment marche import

import "chemin" exécute un autre fichier et rend ce que ce fichier renvoie. Les règles, toutes visibles dans l'exemple ci-dessous :

  • Le chemin est relatif au dossier du fichier qui importe : dans /boutique/main, import "lib/prix" charge /boutique/lib/prix, et dans /boutique/lib/prix, import "monnaie" charge /boutique/lib/monnaie, son voisin. Un chemin qui commence par / part de la racine. À l'invite (et dans les exemples de cette page), un chemin relatif part du dossier courant.
  • Un fichier ne s'exécute qu'une fois par programme. L'importer de nouveau, d'où que ce soit, rend la même valeur sans le relancer : monnaie ci-dessous n'affiche « chargement de monnaie » qu'une fois, bien que deux fichiers l'importent. Tous les fichiers qui importent une bibliothèque partagent la même table, et le même état.
  • **Les variables local du fichier restent à lui ; ses globales sont vues partout.** Une bibliothèque déclare donc une table local M = {}, y range ses fonctions, et finit par return M.
Brass
-- trois petits fichiers, écrits ici pour que l'exemple tourne n'importe où
fs.write("/boutique/lib/monnaie", 'print("chargement de monnaie")\nreturn {ecrire = function(n) return n .. " pièces" end}')
fs.write("/boutique/lib/prix", 'local monnaie = import "monnaie"\nreturn {fer = monnaie.ecrire(4)}')
fs.write("/boutique/main", 'local prix = import "lib/prix"\nlocal monnaie = import "lib/monnaie"\nprint("fer : " .. prix.fer)\nprint(monnaie.ecrire(12))')
import "/boutique/main"
local r = pcall(import, "lib/or")
print(r.error)
Écran
chargement de monnaie
fer : 4 pièces
12 pièces
snippet:6: cannot import 'lib/or': no such file

require est la même fonction, pour qui vient de ComputerCraft. Les autres erreurs que vous pourriez croiser :

erreurcause
cannot import 'lib/uii': no such fileune faute de frappe dans le chemin, ou un chemin relatif à un autre dossier que celui que vous croyez
cannot import 'lib': it is a folderle chemin désigne un dossier, pas un fichier
circular import: 'panneau/main' is still being importeddeux fichiers s'importent l'un l'autre : déplacez ce qu'ils partagent dans un troisième
panneau/lib/ui:12: ...une erreur dans le fichier importé : le message donne ce fichier et sa ligne
Attention

Seul import est relatif au fichier. Les chemins donnés à fs sont relatifs au dossier courant : celui du shell quand le programme a été lancé, la racine au démarrage. Dans /panneau/main, fs.read("panneau.cfg") chercherait /panneau.cfg au démarrage, mais /panneau/panneau.cfg si on le lance à la main depuis /panneau. Écrivez les chemins de fs depuis la racine, avec un /, comme ce projet.

Les bibliothèques suivent la règle de Brass sur les fonctions : une fonction ne peut pas utiliser une local de la fonction qui l'entoure. L'état d'une bibliothèque (le chemin du journal, par exemple) est donc gardé dans des variables local du fichier lui-même, que toutes ses fonctions peuvent lire et modifier.

Les fichiers

startup

Une seule ligne : le démarrage lance startup, qui passe la main au programme.

startup
-- startup : lancé au démarrage. Tout le programme vit dans le dossier panneau.
import "panneau/main"

panneau/main

Le programme lui-même se lit comme une table des matières : il importe ses bibliothèques, charge les réglages avec leurs valeurs par défaut, crée un bouton par face nommée dans les réglages, et attend les évènements. Un clic commute une sortie et écrit une ligne au journal ; un message part vers update.traiter, qui ignore tout sauf les nouvelles versions.

panneau/main
-- panneau/main : un panneau de commande. Chaque face nommée dans /panneau.cfg reçoit
-- un bouton qui commute sa sortie de redstone. Lancé par /startup.
local config = import "lib/config"
local ui = import "lib/ui"
local log = import "lib/log"
local update = import "lib/update"

local VERSION = 3

local reglages = config.charger("/panneau.cfg", {
  titre = "PANNEAU DE COMMANDE",
  journal = "/panneau.log",
  taille_journal = 4000,
})
log.regler(reglages.journal, reglages.taille_journal)

-- Un bouton par face qui a un nom dans les réglages : left = Fours
local boutons = {}
for _, face in ipairs(rs.sides()) do
  if type(reglages[face]) == "string" then
    table.insert(boutons, {x = 3, y = 3 + #boutons * 2, l = 26, texte = reglages[face], face = face, actif = false})
  end
end

local function dessiner()
  ui.effacer()
  ui.entete(reglages.titre .. "   v" .. VERSION)
  for _, b in ipairs(boutons) do ui.bouton(b) end
  if #boutons == 0 then
    ui.etat("Ajoutez 'left = Lampes' à /panneau.cfg", term.colors.orange)
  else
    ui.etat("Cliquez un bouton. Journal : " .. reglages.journal)
  end
end

log.info("panneau v" .. VERSION .. " démarré")
dessiner()
while true do
  local e = os.pull_event()
  if e.name == "click" then
    for _, b in ipairs(boutons) do
      if ui.touche(b, e) then
        b.actif = not b.actif
        rs.set(b.face, b.actif)
        ui.bouton(b)
        log.info(b.texte .. (b.actif and " en marche" or " à l'arrêt"))
      end
    end
  elseif e.name == "message" then
    update.traiter(e)
  end
end

rs.sides() donne les six faces : les réglages peuvent nommer n'importe laquelle. Sa valeur est vérifiée avec type(...) == "string" : une ligne comme left = 3 donnerait un nombre, pas le texte d'un bouton.

panneau/lib/config

Le fichier de réglages est du texte simple que le joueur modifie avec edit /panneau.cfg. analyser le lit ligne par ligne : il saute les lignes vides, les commentaires (#) et les lignes sans =, coupe chaque ligne au premier =, enlève les espaces, et convertit les nombres et true/false. charger complète le résultat avec les valeurs par défaut, et écrit un premier fichier fait de ces valeurs quand il n'y en a pas, pour que le joueur ait de quoi partir.

panneau/lib/config
-- panneau/lib/config : lit un fichier de réglages fait de lignes "clé = valeur".
-- Les lignes qui commencent par # sont des commentaires ; nombres et true/false sont convertis.
local M = {}

-- "12" donne 12, "true" donne true, tout le reste reste du texte
local function convertir(texte)
  if texte == "true" then return true end
  if texte == "false" then return false end
  local n = tonumber(texte)
  if n then return n end
  return texte
end

-- Les réglages écrits dans un texte, sous forme de table
function M.analyser(texte)
  local reglages = {}
  for _, brute in ipairs(string.split(texte, "\n")) do
    local ligne = string.trim(brute)
    local egal = string.find(ligne, "=")
    if egal and not string.starts(ligne, "#") then
      local cle = string.trim(string.sub(ligne, 1, egal - 1))
      reglages[cle] = convertir(string.trim(string.sub(ligne, egal + 1)))
    end
  end
  return reglages
end

-- Écrit les réglages dans un fichier que le joueur peut modifier
function M.enregistrer(chemin, reglages)
  local lignes = {"# Réglages : une ligne 'clé = valeur' par réglage"}
  for k, v in pairs(reglages) do
    table.insert(lignes, k .. " = " .. tostring(v))
  end
  fs.write(chemin, table.concat(lignes, "\n") .. "\n")
end

-- Les réglages de chemin, complétés par les valeurs par défaut. Un fichier absent est
-- créé à partir d'elles, pour qu'il y ait quelque chose à modifier.
function M.charger(chemin, defauts)
  local texte = fs.read(chemin)
  if texte == nil then
    M.enregistrer(chemin, defauts)
    texte = ""
  end
  local reglages = M.analyser(texte)
  for k, v in pairs(defauts) do
    if reglages[k] == nil then reglages[k] = v end
  end
  return reglages
end

return M

M.charger appelle M.enregistrer, défini plus haut dans le fichier. Ce sont deux champs de M, cherchés au moment de l'appel : l'ordre des définitions n'aurait même pas d'importance.

Après un premier lancement, et trois lignes ajoutées à la main, /panneau.cfg contient :

/panneau.cfg
# Réglages : une ligne 'clé = valeur' par réglage
titre = HALLE DE FONTE
journal = /panneau.log
taille_journal = 4000
left = Fours
right = Convoyeur
top = Lumières

panneau/lib/ui

De petites fonctions de dessin pour un écran de texte, les mêmes dans tous les programmes : une barre de titre, un bouton qui sait s'il est en marche, un test pour les clics, une ligne d'état. Un bouton est une table : le programme peut y garder son état (actif), et M.touche compare un clic avec la position du bouton lui-même.

panneau/lib/ui
-- panneau/lib/ui : des aides au dessin pour les écrans de texte.
local M = {}
local couleurs = term.colors

function M.effacer()
  term.set_bg(couleurs.black)
  term.clear()
end

-- Une barre bleue en haut de l'écran
function M.entete(titre)
  term.set_cursor(1, 1)
  term.set_bg(couleurs.blue)
  term.set_fg(couleurs.white)
  term.write(" " .. titre .. string.rep(" ", term.get_size().w))   -- coupé au bord
  term.set_bg(couleurs.black)
end

-- Un bouton est une table {x =, y =, l =, texte =, actif =} : vert en marche, rouge à l'arrêt
function M.bouton(b)
  local etat = b.actif and "MARCHE" or "ARRÊT"
  local texte = " " .. b.texte
  term.set_cursor(b.x, b.y)
  term.set_bg(b.actif and couleurs.green or couleurs.red)
  term.set_fg(couleurs.white)
  term.write(texte .. string.rep(" ", b.l - #texte - #etat - 1) .. etat .. " ")
  term.set_bg(couleurs.black)
end

-- Le clic e est-il tombé sur le bouton ?
function M.touche(b, e)
  return e.y == b.y and e.x >= b.x and e.x < b.x + b.l
end

-- Une ligne de texte en bas de l'écran
function M.etat(texte, couleur)
  term.set_cursor(1, term.get_size().h)
  term.set_bg(couleurs.black)
  term.clear_line()
  term.set_fg(couleur or couleurs.light_gray)
  term.write(texte)
end

return M

panneau/lib/log

Un journal répond à « qui a arrêté le convoyeur cette nuit ? ». Chaque ligne commence par l'heure du monde. Quand le fichier dépasse octets_max, il est renommé avec .old (à la place de l'ancien) et un nouveau commence : le journal ne prend jamais plus du double de sa taille sur le disque.

panneau/lib/log
-- panneau/lib/log : un journal de lignes datées qui ne grossit jamais trop.
local M = {}
local chemin = "/programme.log"   -- changé par M.regler
local octets_max = 4000

function M.regler(fichier, taille)
  chemin, octets_max = fichier, taille
end

local function heure()
  local t = (os.day_time() + 6000) % 24000
  return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end

local function ajouter(ligne)
  local taille = fs.size(chemin)
  if taille and taille > octets_max then
    fs.move(chemin, chemin .. ".old")   -- remplace l'ancien journal précédent
  end
  fs.append(chemin, ligne .. "\n")
end

-- Un disque plein ne doit pas arrêter le programme : la ligne est perdue, c'est tout
function M.ecrire(niveau, texte)
  pcall(ajouter, heure() .. " " .. niveau .. " " .. texte)
end

function M.info(texte)
  M.ecrire("INFO", texte)
end

function M.alerte(texte)
  M.ecrire("ALERTE", texte)
end

return M

M.regler change chemin et octets_max, deux variables local du fichier : toutes les fonctions de la bibliothèque voient les nouvelles valeurs. Et comme une bibliothèque ne s'exécute qu'une fois, lib/update, qui importe log lui aussi, écrit dans le fichier choisi par panneau/main.

L'écriture passe par pcall : un disque plein fait perdre une ligne de journal plutôt que d'arrêter le panneau.

panneau/lib/update

Elle attend un message sur le canal maj qui porte le mot secret et une table de fichiers, écrit chaque fichier, répond, et redémarre pour que la nouvelle version parte du début. La réponse part vers l'expéditeur avant le redémarrage : net.send remet le message tout de suite, et os.reboot ne prend effet qu'à la fin du tick.

panneau/lib/update
-- panneau/lib/update : installe une nouvelle version du programme envoyée par "publier".
local log = import "log"   -- panneau/lib/log : le même module que celui de main

local M = {}
local SECRET = "atelier-laiton-7"
local CANAL = "maj"

local function installer(fichiers)
  for chemin, texte in pairs(fichiers) do
    fs.write("/" .. chemin, texte)
  end
end

-- Appelée par main pour chaque évènement "message" : true si c'était une nouvelle version
function M.traiter(e)
  local d = e.data
  if e.channel ~= CANAL or type(d) ~= "table" or d.secret ~= SECRET or type(d.fichiers) ~= "table" then
    return false
  end
  local r = pcall(installer, d.fichiers)
  if not r.ok then
    log.alerte("mise à jour " .. tostring(d.version) .. " ratée : " .. r.error)
    net.send(e.sender, {secret = SECRET, version = d.version, ok = false, erreur = r.error}, CANAL)
    return true
  end
  log.info("mis à jour en version " .. tostring(d.version) .. " par n°" .. e.sender)
  net.send(e.sender, {secret = SECRET, version = d.version, ok = true}, CANAL)
  os.reboot()   -- les nouveaux fichiers repartent du début
  return true
end

return M

Si l'écriture échoue en route (not enough space), certains fichiers sont neufs et d'autres anciens. La bibliothèque répond alors avec l'erreur au lieu de redémarrer : l'ancienne version continue de tourner en mémoire, et vous pouvez faire de la place et publier de nouveau.

À quoi ça ressemble

L'écran du panneau, dessiné par les fonctions de lib/ui (recopiées dans l'exemple, puisque cette page n'a pas de disque d'où importer) avec les trois sorties des réglages ci-dessus :

démo du panneau
local M = {}
local couleurs = term.colors
function M.effacer()
  term.set_bg(couleurs.black)
  term.clear()
end
function M.entete(titre)
  term.set_cursor(1, 1)
  term.set_bg(couleurs.blue)
  term.set_fg(couleurs.white)
  term.write(" " .. titre .. string.rep(" ", term.get_size().w))
  term.set_bg(couleurs.black)
end
function M.bouton(b)
  local etat = b.actif and "MARCHE" or "ARRÊT"
  local texte = " " .. b.texte
  term.set_cursor(b.x, b.y)
  term.set_bg(b.actif and couleurs.green or couleurs.red)
  term.set_fg(couleurs.white)
  term.write(texte .. string.rep(" ", b.l - #texte - #etat - 1) .. etat .. " ")
  term.set_bg(couleurs.black)
end
function M.etat(texte, couleur)
  term.set_cursor(1, term.get_size().h)
  term.set_bg(couleurs.black)
  term.clear_line()
  term.set_fg(couleur or couleurs.light_gray)
  term.write(texte)
end

local ui = M
ui.effacer()
ui.entete("HALLE DE FONTE   v3")
ui.bouton({x = 3, y = 3, l = 26, texte = "Fours", actif = true})
ui.bouton({x = 3, y = 5, l = 26, texte = "Convoyeur", actif = false})
ui.bouton({x = 3, y = 7, l = 26, texte = "Lumières", actif = true})
ui.etat("Cliquez un bouton. Journal : /panneau.log")
Écran
Écran

Le tester

  1. Créez les dossiers et les fichiers : mkdir panneau, mkdir panneau/lib, puis edit panneau et le bouton + Fichier pour chaque fichier (ou edit panneau/lib/ui pour chacun). Écrivez startup à la racine.
  2. Redémarrez (Ctrl+R). Le premier lancement crée /panneau.cfg et vous invite à ajouter une sortie. Modifiez-le, ajoutez left = Fours, redémarrez : le bouton apparaît.
  3. Cliquez-le : la sortie commute, et le journal le raconte :

    Terminal
    > cat panneau.log
    18:02 INFO panneau v3 démarré
    18:03 INFO Fours en marche
    18:05 INFO Fours à l'arrêt
  4. Faites une erreur exprès : renommez panneau/lib/ui en panneau/lib/uii avec mv. Le démarrage s'arrête sur panneau/main:4: cannot import 'lib/ui': no such file : le message donne le fichier et la ligne de l'import.

Mettre à jour beaucoup d'ordinateurs

Une fois le panneau installé dans chaque halle de l'usine, chaque correction doit atteindre chaque copie. Deux façons.

Par disquette : cloner

Les fichiers d'un ordinateur vivent sur son disque, et chaque ordinateur garde son disque dans son lecteur. cloner copie des fichiers d'un disque vers d'autres, sur un seul ordinateur. Il lit tout en mémoire tant que le disque d'origine est dans le lecteur, puis attend que vous changiez de disque : le programme continue de tourner quand son disque est éjecté, puisqu'il est déjà chargé, et un évènement disk lui dit quand un disque sort (e.inserted vaut false) et quand un autre entre (true). Il ne reste qu'à porter chaque nouveau disque à son ordinateur.

cloner
-- cloner : copie des fichiers et des dossiers sur d'autres disques.
-- Usage, depuis la racine : cloner startup panneau   puis changez de disque quand il le demande.
if #arg == 0 then
  print("Usage : cloner <fichier ou dossier> ...")
  return
end

-- 1. Tout est lu en mémoire, tant que le disque d'origine est dans le lecteur
local fichiers = {}
local nombre, octets = 0, 0
local function rassembler(chemin)
  if fs.is_dir(chemin) then
    for _, nom in ipairs(fs.list(chemin)) do
      rassembler(chemin .. "/" .. nom)
    end
  else
    local texte = fs.read(chemin)
    if texte == nil then error("pas de fichier '" .. chemin .. "'") end
    fichiers[chemin] = texte
    nombre, octets = nombre + 1, octets + #texte
  end
end
for _, nom in ipairs(arg) do
  rassembler(nom)
end
print(nombre .. " fichiers lus, " .. octets .. " octets.")

local function tout_ecrire()
  for chemin, texte in pairs(fichiers) do
    fs.write(chemin, texte)
  end
end

-- 2. Un évènement "disk" arrive quand un disque quitte le lecteur et quand un autre y entre
while true do
  print("Éjectez le disque (accroupi + clic droit main vide) et insérez-en un neuf. Ctrl+T pour arrêter.")
  repeat
    local e = os.pull_event("disk")
  until e.inserted
  local r = pcall(tout_ecrire)
  if r.ok then
    print("Copié. " .. fs.free() .. " octets libres sur ce disque.")
  else
    print("Échec : " .. r.error)
  end
end

Lancez-le depuis la racine avec les noms à copier : cloner startup panneau. Il ne copie pas /panneau.cfg : chaque disque garde ses propres réglages. Un ordinateur n'accepte que les disques de son âge ou plus anciens (un Micro-ordinateur lit les disquettes), et le Microcontrôleur n'a pas de lecteur du tout : pour lui, passez par le réseau.

Par le réseau : publier et recevoir

L'ordinateur de l'atelier garde la copie de référence. publier rassemble startup et tous les fichiers de /panneau dans une table, trouve les ordinateurs dont l'étiquette commence par panneau (net.computers donne le label de chacun), et leur envoie la table avec net.send. Puis il attend leurs réponses et renvoie aux silencieux, trois fois au plus : le même protocole de réponses, de délais et de renvois que dans Télécommande par le réseau.

publier
-- publier : envoie le programme à tous les ordinateurs dont l'étiquette commence par
-- "panneau", et attend la réponse de chacun. Usage : publier 4   (le numéro de version)
local SECRET = "atelier-laiton-7"
local CANAL = "maj"
local PREFIXE = "panneau"   -- les étiquettes des ordinateurs à mettre à jour
local DELAI = 3             -- secondes d'attente des réponses
local ESSAIS = 3

local version = tonumber(arg[1])
if version == nil then
  print("Usage : publier <numéro de version>")
  return
end

-- Tous les fichiers d'un dossier et de ses sous-dossiers : fichiers["panneau/lib/ui"] = "..."
local function rassembler(dossier, fichiers)
  for _, nom in ipairs(fs.list("/" .. dossier)) do
    local chemin = dossier .. "/" .. nom
    if fs.is_dir("/" .. chemin) then
      rassembler(chemin, fichiers)
    else
      fichiers[chemin] = fs.read("/" .. chemin)
    end
  end
end

local fichiers = {startup = fs.read("/startup")}
rassembler("panneau", fichiers)
local taille = 0
for _, texte in pairs(fichiers) do taille = taille + #texte end
if taille > 30000 then error("le programme est trop gros pour un seul message : " .. taille .. " caractères") end

-- Les ordinateurs à mettre à jour, trouvés par leur étiquette
local cibles = {}
local nombre = 0
for _, c in ipairs(net.computers()) do
  if c.label and string.starts(c.label, PREFIXE) then
    cibles[c.id] = {etiquette = c.label, essais = 0, fini = false}
    nombre = nombre + 1
  end
end
print("Version " .. version .. ", " .. taille .. " caractères, vers " .. nombre .. " ordinateur(s)")

local message = {secret = SECRET, version = version, fichiers = fichiers}

-- Envoie à ceux qui n'ont pas répondu ; renvoie le nombre d'envois
local function envoyer_tour()
  local envoyes = 0
  for id, c in pairs(cibles) do
    if not c.fini and c.essais < ESSAIS then
      c.essais = c.essais + 1
      net.send(id, message, CANAL)
      envoyes = envoyes + 1
    end
  end
  return envoyes
end

local function tous_finis()
  for _, c in pairs(cibles) do
    if not c.fini then return false end
  end
  return true
end

-- Une réponse : {secret =, version =, ok =, erreur =} venue de l'une des cibles
local function sur_reponse(e)
  local d = e.data
  if e.name ~= "message" or e.channel ~= CANAL or cibles[e.sender] == nil then return end
  if type(d) ~= "table" or d.secret ~= SECRET or d.version ~= version then return end
  local c = cibles[e.sender]
  c.fini = true
  if d.ok then
    print("n°" .. e.sender .. " " .. c.etiquette .. " : à jour")
  else
    print("n°" .. e.sender .. " " .. c.etiquette .. " : échec, " .. tostring(d.erreur))
  end
end

while envoyer_tour() > 0 do
  local minuterie = os.start_timer(DELAI)
  while not tous_finis() do
    local e = os.pull_event()
    if e.name == "timer" and e.id == minuterie then break end
    sur_reponse(e)
  end
end

for id, c in pairs(cibles) do
  if not c.fini then print("n°" .. id .. " " .. c.etiquette .. " : pas de réponse") end
end

Un message porte 32 768 caractères au plus (sinon message too large), d'où la vérification de taille : au-delà, envoyez un fichier par message. Un ordinateur qui reçoit un gros message doit aussi avoir de la place pour lui dans sa file d'évènements, qui peut prendre un quart de sa mémoire : environ 30 Ko de texte sur un Microcontrôleur, dont la mémoire ne contient de toute façon que 16 Ko.

Sur les ordinateurs qui font déjà tourner le panneau, lib/update reçoit la nouvelle version. Un ordinateur tout neuf n'a encore rien pour la recevoir : tapez une fois ce court programme recevoir, donnez une étiquette à l'ordinateur, et lancez-le.

recevoir
-- recevoir : à taper une fois sur un nouvel ordinateur, après lui avoir donné une
-- étiquette (label panneau-4), puis lancez publier à l'atelier. Il installe et redémarre.
local SECRET = "atelier-laiton-7"
print("J'attends le programme, je suis le n°" .. net.id())
while true do
  local m = net.receive()
  local d = m.data
  if m.channel == "maj" and type(d) == "table" and d.secret == SECRET and type(d.fichiers) == "table" then
    for chemin, texte in pairs(d.fichiers) do
      fs.write("/" .. chemin, texte)
    end
    net.send(m.sender, {secret = SECRET, version = d.version, ok = true}, "maj")
    os.reboot()
  end
end
Terminal
> label panneau-4
> recevoir
J'attends le programme, je suis le n°23

Puis, à l'atelier :

Terminal
> publier 4
Version 4, 6641 caractères, vers 4 ordinateur(s)
n°23 panneau-4 : à jour
n°12 panneau-1 : à jour
n°15 panneau-2 : à jour
n°17 panneau-3 : pas de réponse

Ici, panneau-3 n'a pas répondu trois fois : il est éteint, hors de portée, son chunk n'est pas chargé, ou son panneau ne tourne pas (un ordinateur revenu à l'invite du shell perd le message, alors que net.send renvoie true). Publiez de nouveau plus tard ; les autres installeront simplement la même version une fois de plus.

Important

Qui connaît le mot secret peut installer n'importe quel programme sur vos ordinateurs. Gardez-le long, ne l'envoyez jamais avec net.broadcast, et changez-le en même temps dans publier, recevoir et lib/update.

Conseils pour les gros programmes

  • **Gardez main court.** Quand une partie grossit, déplacez-la dans une bibliothèque au nom qui dit ce qu'elle fait. Une bonne bibliothèque s'explique en une phrase.
  • Pas de globales. Une globale posée dans un fichier est vue par tous les autres, et deux bibliothèques qui utilisent toutes deux compte s'écraseront l'une l'autre. Utilisez local, et renvoyez une table.
  • Un numéro de version affiché à l'écran (v3 dans la barre de titre) dit d'un coup d'œil quels ordinateurs sont à jour.
  • Testez une bibliothèque seule à l'invite brass : ui = import "/panneau/lib/ui", puis appelez ses fonctions une à une. Chaque ligne de l'invite est un morceau à part : utilisez-y une globale, pas une local.