Create: Computing AgesDoc Brass
Recettes : vrais projets

Éclairage jour et nuit

Allumer les lampes de plusieurs zones au crépuscule et les éteindre à l'aube par des fréquences de Liaison de redstone, avec un bouton pour forcer et un écran d'état qui affiche l'heure.

Des réverbères qui s'allument quand le soleil se couche, une gare qui reste éclairée un peu plus longtemps, des vitrines qui s'éteignent à 23:00, et tout éteint le matin : pas de capteurs de lumière partout, un seul ordinateur pour toute la ville. Cette recette pilote plusieurs zones de lampes à travers des Liaisons de redstone de Create, chacune avec ses horaires, avec un bouton pour tout forcer allumé ou éteint et un écran d'état qui affiche l'heure en gros chiffres.

C'est un bon exemple de programme guidé par l'horloge du monde : il ne compte jamais les heures lui-même, il regarde os.day_time et en déduit ce qui doit être allumé maintenant. Dormir dans un lit, /time set ou un redémarrage du serveur ne peuvent pas le tromper.

Ce qu'il vous faut

  • Un ordinateur à écran couleur. Un Microcontrôleur est parfait : bon marché, en couleurs, avec sa propre mémoire de 16 Ko, entraîné par un arbre par-dessous. L'écran est prévu pour ses 40 × 12 caractères et tient aussi sur les plus grands. Tout ordinateur à partir de l'Ordinateur à transistors peut le faire tourner (la grande horloge a besoin de gfx), mais seuls ceux en couleur montrent les zones allumées en jaune.
  • Une Liaison de redstone par zone, en mode réception, à côté des lampes de la zone (ou reliée à elles). Ses deux emplacements portent les deux objets de la fréquence de la zone, les mêmes que dans le programme : de la poudre de pierre lumineuse et un lingot de fer pour les rues, par exemple. L'ordinateur émet lui-même sur cette fréquence avec link.set : il n'a pas besoin de bloc de liaison à lui. Les zones doivent être à portée de Liaison de redstone de l'ordinateur (le linkRange de Create, 256 blocs par défaut).
  • Un bouton sur le dessus de l'ordinateur, pour forcer à la main.
  • Si vous voulez, des moniteurs contre l'ordinateur, pour lire l'heure de loin.
Salle de contrôle (vue de côté) :      Dans chaque zone :

     [bouton]                          [Liaison de redstone]  réception : poudre lumineuse + lingot de fer
       [MC]   Microcontrôleur          [lampe][lampe][lampe] (alimentées par la liaison)
     ==arbre== par-dessous

Comment ça marche

1. Les réglages

Chaque zone a un nom pour l'écran, les deux objets de sa fréquence, et ses horaires : quand elle s'allume (allume) et quand elle s'éteint (eteint), écrits comme sur une horloge.

Brass
local ZONES = {
  {nom = "RUES", a = "minecraft:glowstone_dust", b = "minecraft:iron_ingot", allume = "18:30", eteint = "05:30"},
  {nom = "GARE", a = "minecraft:glowstone_dust", b = "minecraft:gold_ingot", allume = "18:00", eteint = "06:30"},
  {nom = "USINE", a = "minecraft:glowstone_dust", b = "minecraft:copper_ingot", allume = "19:00", eteint = "05:00"},
  {nom = "BOUTIQUES", a = "minecraft:glowstone_dust", b = "minecraft:emerald", allume = "18:30", eteint = "23:00"},
}

Dans Minecraft, le soleil se couche vers 18:00 et il fait nuit à partir de 19:00 environ ; le ciel s'éclaircit vers 05:00 et le jour commence à 06:00. Un ordinateur peut émettre sur 32 fréquences au plus : 32 zones, donc.

2. L'horloge du monde

os.day_time donne l'heure du jour en ticks, de 0 à 23999. Le jour commence à 6:00, pas à minuit, et une heure fait 1000 ticks : 18:30 vaut 12500, minuit 18000, 05:30 23500. Deux petites fonctions convertissent dans les deux sens :

Brass
local function ticks_de(hhmm)
  local p = string.split(hhmm, ":")
  local h, m = tonumber(p[1]), tonumber(p[2])
  if #p ~= 2 or h == nil or m == nil then error("heure invalide '" .. hhmm .. "', écrivez-la comme 18:30") end
  return ((h - 6) % 24) * 1000 + m * 1000 // 60
end

local function heure_de(t)
  return string.format("%02d:%02d", (t // 1000 + 6) % 24, t % 1000 * 60 // 1000)
end

Une faute de frappe dans les réglages ("1830") arrête aussitôt le programme avec un message clair, plutôt que d'allumer la mauvaise zone à une heure bizarre.

Le cœur du programme est entre : l'heure est-elle dans la plage d'une zone ? Une plage passe d'ordinaire par minuit (18:30 à 05:30), et en ticks elle peut aussi passer par 6:00, où le compte repart de 0. Quand le début est après la fin, la plage fait le tour : l'heure en fait partie si elle est après le début ou avant la fin.

Brass
local function entre(maintenant, debut, fin)
  if debut <= fin then return maintenant >= debut and maintenant < fin end
  return maintenant >= debut or maintenant < fin
end

3. Les lampes

voulu tranche pour une zone : toujours allumée ou toujours éteinte dans un mode forcé, ses horaires sinon. mettre_a_jour compare avec l'état actuel de la zone (z.allumee) et n'appelle link.set que s'il change, avec true ou false (15 ou 0). Le premier appel après le démarrage envoie toutes les zones : les lampes sont justes dès le départ.

Brass
local function mettre_a_jour(maintenant)
  for _, z in ipairs(ZONES) do
    local allumee = voulu(z, maintenant)
    if allumee ~= z.allumee then
      z.allumee = allumee
      link.set(z.a, z.b, allumee)
    end
  end
end

link.set arrête le programme avec unknown item '...' quand un identifiant d'objet est faux : vérifiez d'abord ceux des réglages si cela arrive.

4. Le bouton

Un bouton envoie un évènement redstone quand on l'enfonce et un autre quand il ressort. Pour n'agir qu'une fois par appui, le programme retient l'entrée à l'évènement précédent et ne réagit que quand elle passe d'éteinte à allumée :

Brass
local function sur_redstone()
  local enfonce = rs.get(BOUTON) > 0
  if enfonce and not appuye then mode_suivant() end
  appuye = enfonce
end

Chaque appui passe au mode suivant : AUTO, TOUT ALLUMÉ, TOUT ÉTEINT, puis AUTO à nouveau. Un clic sur la ligne du mode à l'écran fait de même, pratique sur un moniteur.

5. L'écran d'état

L'heure est dessinée avec gfx.text en taille 4, avec un soleil ou une lune à côté (deux disques : un blanc, et un noir qui le croque). Elle n'est redessinée que quand la minute change. Les zones sont du texte simple, en jaune quand elles sont allumées, avec leurs horaires, et la dernière ligne annonce la suite, trouvée par prochain_changement : le début ou la fin la plus proche parmi les zones, compté avec % 24000 pour qu'une heure après minuit vienne après 23:00.

6. La boucle principale

Chaque tour de boucle met à jour les lampes et l'écran, puis attend un évènement : la minuterie d'une seconde, le bouton ou un clic. Une minute du monde dure moins d'une seconde réelle (1000 ticks par heure, 20 ticks par seconde) : l'horloge de l'écran avance d'une ou deux minutes à chaque tour.

Le programme complet

startup
-- Éclairage jour et nuit : les lampes de chaque zone s'allument au crépuscule et
-- s'éteignent à l'aube, par des fréquences de Liaison de redstone de Create. Un bouton sur
-- l'ordinateur (ou un clic sur la ligne du mode) passe de AUTO à TOUT ALLUMÉ et TOUT ÉTEINT.

-- 1. Réglages : chaque zone est une fréquence (ses deux objets) et ses horaires
local ZONES = {
  {nom = "RUES", a = "minecraft:glowstone_dust", b = "minecraft:iron_ingot", allume = "18:30", eteint = "05:30"},
  {nom = "GARE", a = "minecraft:glowstone_dust", b = "minecraft:gold_ingot", allume = "18:00", eteint = "06:30"},
  {nom = "USINE", a = "minecraft:glowstone_dust", b = "minecraft:copper_ingot", allume = "19:00", eteint = "05:00"},
  {nom = "BOUTIQUES", a = "minecraft:glowstone_dust", b = "minecraft:emerald", allume = "18:30", eteint = "23:00"},
}
local BOUTON = "top"
local MODES = {"AUTO", "TOUT ALLUMÉ", "TOUT ÉTEINT"}

local mode = 1             -- numéro dans MODES
local appuye = false       -- l'entrée du bouton au dernier évènement : un appui passe d'éteint à allumé
local heure_affichee = nil -- l'heure à l'écran, redessinée quand elle change

-- 2. L'heure : os.day_time compte les ticks depuis 6:00, 1000 ticks par heure
local function ticks_de(hhmm)
  local p = string.split(hhmm, ":")
  local h, m = tonumber(p[1]), tonumber(p[2])
  if #p ~= 2 or h == nil or m == nil then error("heure invalide '" .. hhmm .. "', écrivez-la comme 18:30") end
  return ((h - 6) % 24) * 1000 + m * 1000 // 60
end

local function heure_de(t)
  return string.format("%02d:%02d", (t // 1000 + 6) % 24, t % 1000 * 60 // 1000)
end

-- maintenant est-il dans [debut, fin) ? La plage peut passer par 6:00, où les ticks repartent de 0
local function entre(maintenant, debut, fin)
  if debut <= fin then return maintenant >= debut and maintenant < fin end
  return maintenant >= debut or maintenant < fin
end

-- 3. Les lampes
local function voulu(z, maintenant)
  if MODES[mode] == "TOUT ALLUMÉ" then return true end
  if MODES[mode] == "TOUT ÉTEINT" then return false end
  return entre(maintenant, z.debut, z.fin)
end

-- N'envoie une fréquence que quand sa zone change
local function mettre_a_jour(maintenant)
  for _, z in ipairs(ZONES) do
    local allumee = voulu(z, maintenant)
    if allumee ~= z.allumee then
      z.allumee = allumee
      link.set(z.a, z.b, allumee)
    end
  end
end

local function mode_suivant()
  mode = mode % #MODES + 1
end

-- Le prochain changement en mode AUTO : le début ou la fin la plus proche parmi les zones
local function prochain_changement(maintenant)
  local meilleur, texte = 24000, ""
  for _, z in ipairs(ZONES) do
    local quand, quoi = z.debut, ", allumage " .. z.allume
    if z.allumee then quand, quoi = z.fin, ", extinction " .. z.eteint end
    local attente = (quand - maintenant) % 24000
    if attente < meilleur then meilleur, texte = attente, z.nom .. quoi end
  end
  return texte
end

-- Le bouton : un évènement "redstone" arrive à l'appui et au relâchement
local function sur_redstone()
  local enfonce = rs.get(BOUTON) > 0
  if enfonce and not appuye then mode_suivant() end
  appuye = enfonce
end

-- 4. L'écran : une grande horloge et le ciel en gfx, les zones en texte
local W = gfx.size().w

local function dessiner_ciel(maintenant)
  local x, y = W - 16, 14
  gfx.rect(x - 13, y - 13, 27, 27, "black", true)
  if entre(maintenant, 12500, 23500) then
    gfx.circle(x, y, 8, "white", true)          -- la lune : un disque...
    gfx.circle(x + 4, y - 3, 7, "black", true)  -- ...croqué d'un côté
  else
    gfx.circle(x, y, 5, "yellow", true)         -- le soleil et ses rayons
    for k = 0, 7 do
      local a = k * math.pi / 4
      gfx.line(x + math.round(8 * math.cos(a)), y + math.round(8 * math.sin(a)),
        x + math.round(11 * math.cos(a)), y + math.round(11 * math.sin(a)), "yellow")
    end
  end
end

local function ecrire_en(x, y, texte, couleur)
  term.set_cursor(x, y)
  term.set_fg(couleur)
  term.write(texte)
end

local function dessiner(maintenant)
  local heure = heure_de(maintenant)
  if heure ~= heure_affichee then
    gfx.rect(1, 1, W - 30, 26, "black", true)
    gfx.text(6, 5, heure, "white", 4)
    dessiner_ciel(maintenant)
    heure_affichee = heure
  end
  term.set_cursor(1, 4)
  term.clear_line()
  local couleur = term.colors.lime
  if mode > 1 then couleur = term.colors.orange end
  ecrire_en(2, 4, "Mode : " .. MODES[mode], couleur)
  ecrire_en(22, 4, "(bouton : suivant)", term.colors.gray)
  for i, z in ipairs(ZONES) do
    local y = 5 + i
    term.set_cursor(1, y)
    term.clear_line()
    local c = term.colors.gray
    if z.allumee then c = term.colors.yellow end
    ecrire_en(2, y, z.nom, c)
    ecrire_en(12, y, z.allumee and "ALLUMÉ" or "éteint", c)
    ecrire_en(20, y, z.allume .. "-" .. z.eteint, term.colors.light_gray)
  end
  term.set_cursor(1, 11)
  term.clear_line()
  if mode == 1 then
    ecrire_en(2, 11, "Ensuite : " .. prochain_changement(maintenant), term.colors.light_gray)
  else
    ecrire_en(2, 11, "Forcé : bouton pour revenir en AUTO", term.colors.orange)
  end
end

-- 5. Boucle principale : un coup d'œil à l'horloge chaque seconde, le bouton tout de suite
for _, z in ipairs(ZONES) do
  z.debut, z.fin = ticks_de(z.allume), ticks_de(z.eteint)
end
term.clear()
gfx.clear()
appuye = rs.get(BOUTON) > 0
local minuterie = os.start_timer(1)
while true do
  local maintenant = os.day_time()
  mettre_a_jour(maintenant)
  dessiner(maintenant)
  local e = os.pull_event()
  if e.name == "timer" and e.id == minuterie then
    minuterie = os.start_timer(1)
  elseif e.name == "redstone" then
    sur_redstone()
  elseif e.name == "click" and e.y == 4 then
    mode_suivant()
  end
end

À quoi ça ressemble

Le même code sur un Microcontrôleur, avec l'heure mise à la main à 19:42 (13700 ticks) au lieu de os.day_time, et sans Liaison de redstone à piloter :

démo de l'écran d'état
local ZONES = {
  {nom = "RUES", allume = "18:30", eteint = "05:30"},
  {nom = "GARE", allume = "18:00", eteint = "06:30"},
  {nom = "USINE", allume = "19:00", eteint = "05:00"},
  {nom = "BOUTIQUES", allume = "18:30", eteint = "23:00"},
}
local MODES = {"AUTO", "TOUT ALLUMÉ", "TOUT ÉTEINT"}
local mode = 1
local heure_affichee = nil

local function ticks_de(hhmm)
  local p = string.split(hhmm, ":")
  return ((tonumber(p[1]) - 6) % 24) * 1000 + tonumber(p[2]) * 1000 // 60
end
local function heure_de(t)
  return string.format("%02d:%02d", (t // 1000 + 6) % 24, t % 1000 * 60 // 1000)
end
local function entre(maintenant, debut, fin)
  if debut <= fin then return maintenant >= debut and maintenant < fin end
  return maintenant >= debut or maintenant < fin
end
local function prochain_changement(maintenant)
  local meilleur, texte = 24000, ""
  for _, z in ipairs(ZONES) do
    local quand, quoi = z.debut, ", allumage " .. z.allume
    if z.allumee then quand, quoi = z.fin, ", extinction " .. z.eteint end
    local attente = (quand - maintenant) % 24000
    if attente < meilleur then meilleur, texte = attente, z.nom .. quoi end
  end
  return texte
end

local W = gfx.size().w
local function dessiner_ciel(maintenant)
  local x, y = W - 16, 14
  gfx.rect(x - 13, y - 13, 27, 27, "black", true)
  if entre(maintenant, 12500, 23500) then
    gfx.circle(x, y, 8, "white", true)
    gfx.circle(x + 4, y - 3, 7, "black", true)
  else
    gfx.circle(x, y, 5, "yellow", true)
    for k = 0, 7 do
      local a = k * math.pi / 4
      gfx.line(x + math.round(8 * math.cos(a)), y + math.round(8 * math.sin(a)),
        x + math.round(11 * math.cos(a)), y + math.round(11 * math.sin(a)), "yellow")
    end
  end
end
local function ecrire_en(x, y, texte, couleur)
  term.set_cursor(x, y)
  term.set_fg(couleur)
  term.write(texte)
end
local function dessiner(maintenant)
  local heure = heure_de(maintenant)
  if heure ~= heure_affichee then
    gfx.rect(1, 1, W - 30, 26, "black", true)
    gfx.text(6, 5, heure, "white", 4)
    dessiner_ciel(maintenant)
    heure_affichee = heure
  end
  term.set_cursor(1, 4)
  term.clear_line()
  local couleur = term.colors.lime
  if mode > 1 then couleur = term.colors.orange end
  ecrire_en(2, 4, "Mode : " .. MODES[mode], couleur)
  ecrire_en(22, 4, "(bouton : suivant)", term.colors.gray)
  for i, z in ipairs(ZONES) do
    local y = 5 + i
    term.set_cursor(1, y)
    term.clear_line()
    local c = term.colors.gray
    if z.allumee then c = term.colors.yellow end
    ecrire_en(2, y, z.nom, c)
    ecrire_en(12, y, z.allumee and "ALLUMÉ" or "éteint", c)
    ecrire_en(20, y, z.allume .. "-" .. z.eteint, term.colors.light_gray)
  end
  term.set_cursor(1, 11)
  term.clear_line()
  if mode == 1 then
    ecrire_en(2, 11, "Ensuite : " .. prochain_changement(maintenant), term.colors.light_gray)
  else
    ecrire_en(2, 11, "Forcé : bouton pour revenir en AUTO", term.colors.orange)
  end
end

local maintenant = 13700   -- 19:42
for _, z in ipairs(ZONES) do
  z.debut, z.fin = ticks_de(z.allume), ticks_de(z.eteint)
  z.allumee = entre(maintenant, z.debut, z.fin)
end
term.clear()
gfx.clear()
dessiner(maintenant)
Écran
Écran

Le tester

  1. Mettez les Liaisons de redstone en mode réception (accroupi et clic droit, ou clic droit avec une Clé) avec les bons objets, et lancez le programme. À midi, toutes les zones sont éteintes et la dernière ligne affiche Ensuite : GARE, allumage 18:00.
  2. Avancez l'horloge avec les commandes du jeu et regardez les zones suivre en moins d'une seconde :

    Terminal
    /time set 12100
    /time set 12600
    /time set 17100
    /time set 23600

    Soit 18:06 (la gare s'allume), 18:36 (les rues et les boutiques), 23:06 (les boutiques s'éteignent) et 05:36 (seule la gare est encore allumée).

  3. Appuyez sur le bouton : TOUT ALLUMÉ, toutes les lampes s'allument quelle que soit l'heure. Encore : TOUT ÉTEINT. Encore : retour en AUTO, et chaque zone retrouve ses horaires.
  4. Dormez dans un lit la nuit : au matin, les lampes s'éteignent d'elles-mêmes, parce que le programme lit l'heure au lieu de la compter.
  5. Cassez un instant l'arbre de l'ordinateur : le programme se fige avec l'ordinateur et les fréquences gardent leur dernière valeur ; la rotation revenue, il continue. Un redémarrage (Ctrl+R) est différent : toutes les fréquences sont relâchées, les lampes s'éteignent un instant, puis le programme renvoie toutes les zones.

Variantes

  • Un capteur de lumière du jour au lieu de l'horloge. Les orages assombrissent le ciel sans changer l'heure. Posez un capteur de lumière du jour contre l'ordinateur et allumez les rues quand rs.get("left") passe sous un niveau de votre choix : il lit la vraie lumière, pluie comprise.
  • Un forçage par zone. Rendez les lignes des zones cliquables (e.y == 5 + i) et donnez à chaque zone son propre mode, affiché à côté de son nom.
  • Une Liaison d'affichage. display.set({heure_de(maintenant), prochain_changement(maintenant)}) met l'heure et le prochain changement sur un Panneau d'affichage de Create, à la gare, par une Liaison d'affichage posée contre l'ordinateur (display).
  • Une zone proche sans liaison. Pour des lampes juste à côté de l'ordinateur, rs.set("left", allumee) fait le travail d'une fréquence.
  • Prévenir avant la fermeture. Faites clignoter les boutiques à 22:55 : dans mettre_a_jour, quand l'heure est à quelques minutes de z.fin, envoyez true et false une seconde sur deux.