Enregistreur de données et graphique
Enregistrer une valeur chaque minute dans un fichier CSV qui ne grossit jamais trop, et tracer la dernière heure en courbe avec des axes.
Un nombre à l'écran dit où en sont les choses ; une courbe dit où elles vont. Le Coffre à objet renforcé du fer se vide-t-il lentement depuis que vous avez ajouté la seconde chaîne de presses ? Le stress saute-t-il chaque fois que le train décharge ? Cette recette enregistre deux valeurs chaque minute (le stock d'un coffre renforcé et le stress du réseau cinétique) dans un fichier, et trace la dernière heure de chacune en courbe.
Vous verrez comment écrire un journal avec fs.append, comment l'empêcher de remplir le disque (la rotation), comment le relire après un redémarrage, et comment transformer une liste de nombres en graphique avec gfx.line.
Ce qu'il vous faut
- Un Micro-ordinateur (ou un Ordinateur moderne) : des couleurs pour deux courbes, et une disquette (64 Ko) pour le journal. Le programme n'enregistre qu'une fois par minute : la vitesse n'est pas un souci. Sur un Ordinateur moderne, les graphiques s'élargissent d'eux-mêmes.
- Un coffre renforcé ou un coffre contre la face gauche de l'ordinateur : le stock à suivre.
- Un bloc cinétique pour lire le stress. Le plus simple est l'arbre qui fait tourner l'ordinateur : c'est un périphérique cinétique sur la face
back, et son réseau est celui de l'usine quand l'ordinateur est entraîné par l'usine. Pour surveiller un autre réseau, posez un Stressomètre de ce réseau contre l'ordinateur et changezJAUGE. - Si vous voulez, des moniteurs contre l'ordinateur pour voir les graphiques en grand.
Vu de dessus :
[Coffre]-[PC]==== arbre de l'usine (le dos de l'ordinateur)
|
(moniteurs devant, facultatifs)Comment ça marche
1. Les réglages
local COFFRE = "left" -- l'inventaire à compter
local OBJET = "minecraft:iron_ingot" -- l'objet à compter, ou nil pour tout
local JAUGE = "back" -- un bloc cinétique : l'arbre de l'ordinateur fait l'affaire
local PERIODE = 60 -- secondes entre deux relevés
local JOURNAL = "/journaux/usine.csv"
local ANCIEN = "/journaux/usine.ancien.csv"
local OCTETS_MAX = 8000 -- au-delà, le journal devient l'ancien
local POINTS = 60 -- valeurs sur les graphiques : la dernière heureLes chemins commencent par / : ils partent de la racine du disque, quel que soit le dossier courant au lancement du programme. fs.write crée le dossier journaux au premier relevé.
2. Lire les valeurs
Un relevé est une petite table : l'heure, et les deux valeurs. Une valeur illisible (un coffre cassé, une jauge absente) reste nil : le relevé est quand même écrit, avec un champ vide, et la courbe montre un trou à cet endroit. Les deux lectures passent par pcall : un bloc retiré au mauvais moment n'arrête jamais l'enregistreur.
local function lire_valeurs()
local releve = {heure = heure()}
local coffre = peripheral.wrap(COFFRE)
if coffre then
local r = pcall(coffre.count, OBJET)
if r.ok then releve.stock = r.value end
end
local jauge = peripheral.wrap(JAUGE)
if jauge then
local utilise = pcall(jauge.stress)
local capacite = pcall(jauge.capacity)
if utilise.ok and capacite.ok and capacite.value > 0 then
releve.stress = math.round(utilise.value / capacite.value * 100)
end
end
return releve
end@inventory.count avec un identifiant d'objet ne compte que cet objet, sans identifiant il compte tout. Le stress est enregistré en pourcentage de la capacité, plus parlant que des unités de stress.
L'heure est celle du monde, tirée de os.day_time et mise sous la forme hh:mm : 0 vaut 6 h du matin, 1000 ticks font une heure. Gardez en tête qu'une journée de Minecraft dure 20 minutes réelles : un relevé par minute réelle, c'est 1 h 12 de temps du monde entre deux lignes du journal, et les 60 relevés des graphiques couvrent trois journées de Minecraft. L'horloge du monde n'a pas non plus de numéro de jour. C'est pourquoi les graphiques comptent le temps à rebours depuis le dernier relevé (« -15MIN », « MAINTENANT ») au lieu d'afficher des heures. Pour un compteur qui ne fait que grandir, utilisez os.clock, les secondes depuis le démarrage de l'ordinateur (il repart de 0 à chaque redémarrage).
3. Le fichier CSV et sa rotation
Le fichier est au format CSV (« valeurs séparées par des virgules ») : une ligne d'en-tête, puis une ligne par relevé. N'importe quel programme, ou vous avec cat, peut le lire.
heure,stock,stress 18:02,1462,58 19:14,1440,61 20:26,,61
fs.append ajoute une ligne à la fin du fichier, et le crée s'il le faut. Mais un journal qui ne fait que grandir finit par remplir le disque : à un relevé par minute, environ 20 Ko par jour réel. Avant chaque relevé, rotation regarde donc la taille du journal (fs.size, nil tant que le fichier n'existe pas). Au-delà de OCTETS_MAX, le journal est renommé en ancien avec fs.move, après suppression de l'ancien précédent. Deux fichiers de 8 Ko au plus : environ neuf heures de relevés chacun, 16 Ko sur le disque pour toujours.
local function rotation()
local taille = fs.size(JOURNAL)
if taille and taille > OCTETS_MAX then
fs.delete(ANCIEN)
fs.move(JOURNAL, ANCIEN)
end
end
local function enregistrer(r)
rotation()
if not fs.exists(JOURNAL) then fs.write(JOURNAL, "heure,stock,stress\n") end
fs.append(JOURNAL, r.heure .. "," .. champ(r.stock) .. "," .. champ(r.stress) .. "\n")
endchamp écrit nil comme un champ vide plutôt que le mot nil. Quand l'écriture échoue (not enough space, ou pas de disque dans le lecteur), l'erreur est rattrapée par le pcall autour d'enregistrer : l'écran affiche ÉCRITURE DU JOURNAL IMPOSSIBLE, l'ancien journal est supprimé pour faire de la place, et l'enregistreur continue.
4. L'historique en mémoire
Les graphiques ont besoin des 60 derniers relevés, pas du fichier entier. retenir les garde dans une liste, du plus ancien au plus récent, et oublie le plus ancien dès qu'il y en a plus que POINTS. Au lancement, charger_fichier lit l'ancien journal puis le courant et passe chaque ligne à retenir : après un redémarrage, les courbes reviennent comme elles étaient.
local function charger_fichier(chemin)
local texte = fs.read(chemin)
if texte == nil then return end
for _, ligne in ipairs(string.split(texte, "\n")) do
local c = string.split(ligne, ",")
if #c == 3 and c[1] ~= "heure" then
retenir({heure = c[1], stock = tonumber(c[2]), stress = tonumber(c[3])})
end
end
endstring.split garde les champs vides ("20:26,,61" donne trois champs, le deuxième vide), et tonumber("") vaut nil : un trou dans le fichier devient un trou dans la courbe. Le test écarte la ligne d'en-tête et la ligne vide qui suit le dernier saut de ligne.
5. Tracer un graphique
dessiner_graphe trace un graphique dans une bande de l'écran : son titre et sa dernière valeur, les axes, une ligne pointillée au milieu, l'échelle, et la courbe.
L'échelle vient des valeurs : la plus basse et la plus haute de l'historique. Un graphique peut demander une plage minimale : celui du stress montre toujours 0 à 100 %, pour qu'un réseau calme n'ait pas l'air de s'affoler. Quand toutes les valeurs sont égales, la plage est élargie de un pour éviter une division par zéro.
Chaque valeur devient ensuite un point. Le plus récent est toujours sur le bord droit, les autres partent vers la gauche, d'un pas par relevé :
local function x_de(i, n)
return GRAPHE_X + (POINTS - n + i - 1) * (GRAPHE_L - 1) // (POINTS - 1)
endet sa hauteur est sa place entre bas et haut, à l'envers puisque les lignes de pixels descendent :
local y = py + ph - 1 - math.round((v - bas) / (haut - bas) * (ph - 1))gfx.line relie chaque point au précédent. Une valeur manquante remet avant à nil : la ligne s'arrête là et reprend à la valeur suivante. Le point le plus récent reçoit un point blanc, gfx.pixel avec une taille de 3.
Sous les graphiques, dessiner écrit l'axe du temps : une marque tous les 15 relevés, comptée à rebours depuis le plus récent, « -15MIN » pour il y a 15 minutes. Les positions ne dépendent pas de l'horloge, seulement de PERIODE : l'axe suppose un relevé par période, si bien qu'après un long arrêt de l'ordinateur, les plus vieux points sont plus anciens que ce que dit l'axe.
6. La boucle principale
mesurer fait un relevé : lire, écrire, retenir, dessiner. La boucle attend sa minuterie avec os.pull_event("timer") : ce programme n'a rien d'autre à attendre. Comme il ne dessine qu'une fois par minute, tout le graphique est redessiné à chaque fois ; quelques centaines d'instructions, rien pour un Micro-ordinateur.
Le programme complet
-- Enregistreur : chaque minute, écrit le stock d'un coffre renforcé et le stress du réseau
-- cinétique dans un fichier CSV, et trace la dernière heure en deux graphiques.
-- Micro-ordinateur, un coffre renforcé (ou un coffre) à sa gauche, entraîné par l'arbre de l'usine.
-- 1. Réglages
local COFFRE = "left" -- l'inventaire à compter
local OBJET = "minecraft:iron_ingot" -- l'objet à compter, ou nil pour tout
local JAUGE = "back" -- un bloc cinétique : l'arbre de l'ordinateur fait l'affaire
local PERIODE = 60 -- secondes entre deux relevés
local JOURNAL = "/journaux/usine.csv"
local ANCIEN = "/journaux/usine.ancien.csv"
local OCTETS_MAX = 8000 -- au-delà, le journal devient l'ancien
local POINTS = 60 -- valeurs sur les graphiques : la dernière heure
local GRAPHES = {
{cle = "stock", titre = "FER", couleur = "light_blue"},
{cle = "stress", titre = "STRESS %", couleur = "orange", bas = 0, haut = 100},
}
local historique = {} -- les POINTS derniers relevés, du plus ancien au plus récent
local probleme = nil -- affiché en bas de l'écran quand le journal ne peut pas être écrit
local function heure()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
-- 2. Lecture : un relevé est {heure =, stock =, stress =}, nil là où un bloc manque
local function lire_valeurs()
local releve = {heure = heure()}
local coffre = peripheral.wrap(COFFRE)
if coffre then
local r = pcall(coffre.count, OBJET)
if r.ok then releve.stock = r.value end
end
local jauge = peripheral.wrap(JAUGE)
if jauge then
local utilise = pcall(jauge.stress)
local capacite = pcall(jauge.capacity)
if utilise.ok and capacite.ok and capacite.value > 0 then
releve.stress = math.round(utilise.value / capacite.value * 100)
end
end
return releve
end
-- 3. Le fichier : un en-tête, puis une ligne par relevé, "18:30,1234,75"
local function champ(v)
if v == nil then return "" end
return tostring(v)
end
-- Un journal plein devient l'ancien ; l'ancien est supprimé
local function rotation()
local taille = fs.size(JOURNAL)
if taille and taille > OCTETS_MAX then
fs.delete(ANCIEN)
fs.move(JOURNAL, ANCIEN)
end
end
local function enregistrer(r)
rotation()
if not fs.exists(JOURNAL) then fs.write(JOURNAL, "heure,stock,stress\n") end
fs.append(JOURNAL, r.heure .. "," .. champ(r.stock) .. "," .. champ(r.stress) .. "\n")
end
-- L'historique en mémoire : les graphiques n'ont besoin que des POINTS derniers relevés
local function retenir(r)
table.insert(historique, r)
if #historique > POINTS then table.remove(historique, 1) end
end
local function charger_fichier(chemin)
local texte = fs.read(chemin)
if texte == nil then return end
for _, ligne in ipairs(string.split(texte, "\n")) do
local c = string.split(ligne, ",")
if #c == 3 and c[1] ~= "heure" then
retenir({heure = c[1], stock = tonumber(c[2]), stress = tonumber(c[3])})
end
end
end
-- 4. Dessin
local taille = gfx.size()
local W, H = taille.w, taille.h
local GRAPHE_X = 26 -- bord gauche des courbes, après l'échelle
local GRAPHE_L = W - GRAPHE_X - 6
local GRAPHE_H = (H - 12) // #GRAPHES -- un graphique et son titre ; 12 px restent pour le temps
local function court(n)
if math.abs(n) >= 10000 then return math.round(n / 1000) .. "K" end
return tostring(n)
end
-- x de la i-ème de n valeurs : la plus récente est toujours sur le bord droit
local function x_de(i, n)
return GRAPHE_X + (POINTS - n + i - 1) * (GRAPHE_L - 1) // (POINTS - 1)
end
local function dessiner_graphe(g, haut_g)
local py, ph = haut_g + 14, GRAPHE_H - 18
gfx.rect(1, haut_g, W, GRAPHE_H, "black", true)
-- la plage des valeurs, au moins bas..haut quand le graphique en a
local bas, haut, recente = math.huge, -math.huge, nil
for _, r in ipairs(historique) do
local v = r[g.cle]
if v then
bas, haut, recente = math.min(bas, v), math.max(haut, v), v
end
end
if g.bas then bas, haut = math.min(bas, g.bas), math.max(haut, g.haut) end
gfx.text(4, haut_g, g.titre, "white", 2)
if recente == nil then
gfx.text(GRAPHE_X, py + ph // 2 - 2, "PAS ENCORE DE DONNÉES", "gray")
return
end
if haut == bas then haut = bas + 1 end
local valeur = court(recente)
gfx.text(W - 6 - #valeur * 8 + 2, haut_g, valeur, g.couleur, 2)
-- les axes, une ligne pointillée au milieu, l'échelle
gfx.line(GRAPHE_X - 1, py, GRAPHE_X - 1, py + ph, "gray")
gfx.line(GRAPHE_X - 1, py + ph, GRAPHE_X + GRAPHE_L, py + ph, "gray")
for x = GRAPHE_X, GRAPHE_X + GRAPHE_L, 4 do gfx.pixel(x, py + ph // 2, "gray") end
gfx.text(2, py, court(haut), "light_gray")
gfx.text(2, py + ph - 5, court(bas), "light_gray")
-- la courbe : une ligne de chaque valeur à la suivante, coupée là où il en manque une
local px, avant = nil, nil
for i, r in ipairs(historique) do
local v = r[g.cle]
if v then
local x = x_de(i, #historique)
local y = py + ph - 1 - math.round((v - bas) / (haut - bas) * (ph - 1))
if avant then gfx.line(px, avant, x, y, g.couleur) end
px, avant = x, y
else
avant = nil
end
end
if avant then gfx.pixel(px, avant, "white", 3) end
end
-- "15MIN" pour 900 secondes, "30S" pour 30
local function il_y_a(secondes)
if secondes >= 60 then return (secondes // 60) .. "MIN" end
return secondes .. "S"
end
local function dessiner()
for i, g in ipairs(GRAPHES) do dessiner_graphe(g, (i - 1) * GRAPHE_H + 2) end
-- l'axe du temps : une marque tous les 15 relevés, à rebours depuis le plus récent
gfx.rect(1, H - 8, W, 8, "black", true)
for k = 0, POINTS - 1, 15 do
local etiquette = "MAINTENANT"
if k > 0 then etiquette = "-" .. il_y_a(k * PERIODE) end
local x = x_de(POINTS - k, POINTS)
gfx.text(math.min(x - #etiquette * 2, W - #etiquette * 4), H - 6, etiquette, "gray")
end
if probleme then
gfx.rect(1, H - 8, W, 8, "black", true)
gfx.text(W // 2 - #probleme * 2, H - 6, probleme, "red")
end
end
-- 5. Un relevé : lire, écrire dans le fichier, ajouter aux graphiques
local function mesurer()
local r = lire_valeurs()
local ecrit = pcall(enregistrer, r)
if ecrit.ok then
probleme = nil
else
probleme = "ÉCRITURE DU JOURNAL IMPOSSIBLE"
pcall(fs.delete, ANCIEN) -- faire de la place pour le prochain essai
end
retenir(r)
dessiner()
end
-- 6. Boucle principale : l'historique des fichiers, puis un relevé par PERIODE
term.clear()
gfx.clear("black")
charger_fichier(ANCIEN)
charger_fichier(JOURNAL)
mesurer()
local minuterie = os.start_timer(PERIODE)
while true do
local e = os.pull_event("timer")
if e.id == minuterie then
mesurer()
minuterie = os.start_timer(PERIODE)
end
endÀ quoi ça ressemble
Cette démonstration garde le code de dessin du programme et remplit l'historique d'une heure inventée pour l'occasion : une chaîne de presses mange le fer, une seconde chaîne a démarré il y a 40 minutes et charge le réseau, un train a rempli le coffre il y a 25 minutes. Un relevé manque il y a 15 minutes (on agrandissait le coffre) : la courbe s'interrompt à cet endroit.
local POINTS, PERIODE = 60, 60
local GRAPHES = {
{cle = "stock", titre = "FER", couleur = "light_blue"},
{cle = "stress", titre = "STRESS %", couleur = "orange", bas = 0, haut = 100},
}
local probleme = nil
-- une heure de relevés d'exemple, du plus ancien au plus récent
local historique = {}
for i = 1, POINTS do
local r = {stock = 1500 - i * 16 + math.round(30 * math.sin(i)), stress = 52 + math.round(4 * math.sin(i / 2))}
if i > 20 then r.stress = r.stress + 22 end
if i > 35 then r.stock = r.stock + 900 end
if i == 45 then r.stock = nil end
table.insert(historique, r)
end
local taille = gfx.size()
local W, H = taille.w, taille.h
local GRAPHE_X = 26
local GRAPHE_L = W - GRAPHE_X - 6
local GRAPHE_H = (H - 12) // #GRAPHES
local function court(n)
if math.abs(n) >= 10000 then return math.round(n / 1000) .. "K" end
return tostring(n)
end
local function x_de(i, n)
return GRAPHE_X + (POINTS - n + i - 1) * (GRAPHE_L - 1) // (POINTS - 1)
end
local function dessiner_graphe(g, haut_g)
local py, ph = haut_g + 14, GRAPHE_H - 18
gfx.rect(1, haut_g, W, GRAPHE_H, "black", true)
local bas, haut, recente = math.huge, -math.huge, nil
for _, r in ipairs(historique) do
local v = r[g.cle]
if v then
bas, haut, recente = math.min(bas, v), math.max(haut, v), v
end
end
if g.bas then bas, haut = math.min(bas, g.bas), math.max(haut, g.haut) end
gfx.text(4, haut_g, g.titre, "white", 2)
if recente == nil then
gfx.text(GRAPHE_X, py + ph // 2 - 2, "PAS ENCORE DE DONNÉES", "gray")
return
end
if haut == bas then haut = bas + 1 end
local valeur = court(recente)
gfx.text(W - 6 - #valeur * 8 + 2, haut_g, valeur, g.couleur, 2)
gfx.line(GRAPHE_X - 1, py, GRAPHE_X - 1, py + ph, "gray")
gfx.line(GRAPHE_X - 1, py + ph, GRAPHE_X + GRAPHE_L, py + ph, "gray")
for x = GRAPHE_X, GRAPHE_X + GRAPHE_L, 4 do gfx.pixel(x, py + ph // 2, "gray") end
gfx.text(2, py, court(haut), "light_gray")
gfx.text(2, py + ph - 5, court(bas), "light_gray")
local px, avant = nil, nil
for i, r in ipairs(historique) do
local v = r[g.cle]
if v then
local x = x_de(i, #historique)
local y = py + ph - 1 - math.round((v - bas) / (haut - bas) * (ph - 1))
if avant then gfx.line(px, avant, x, y, g.couleur) end
px, avant = x, y
else
avant = nil
end
end
if avant then gfx.pixel(px, avant, "white", 3) end
end
local function il_y_a(secondes)
if secondes >= 60 then return (secondes // 60) .. "MIN" end
return secondes .. "S"
end
local function dessiner()
for i, g in ipairs(GRAPHES) do dessiner_graphe(g, (i - 1) * GRAPHE_H + 2) end
gfx.rect(1, H - 8, W, 8, "black", true)
for k = 0, POINTS - 1, 15 do
local etiquette = "MAINTENANT"
if k > 0 then etiquette = "-" .. il_y_a(k * PERIODE) end
local x = x_de(POINTS - k, POINTS)
gfx.text(math.min(x - #etiquette * 2, W - #etiquette * 4), H - 6, etiquette, "gray")
end
if probleme then
gfx.rect(1, H - 8, W, 8, "black", true)
gfx.text(W // 2 - #probleme * 2, H - 6, probleme, "red")
end
end
gfx.clear("black")
dessiner()
Le tester
- Mettez
PERIODE = 2pour l'essai : un relevé toutes les deux secondes remplit vite les graphiques, et l'axe affiche « -30S », « -1MIN »... Remettez 60 ensuite. Lancez-le, arrêtez-le avec Ctrl+T après quelques relevés et regardez le fichier :
> cat /journaux/usine.csv heure,stock,stress 18:02,1462,58 18:04,1462,58 18:07,1440,61
(Deux secondes réelles font 40 ticks, environ deux minutes et demie du temps du monde.)
- Relancez-le : les courbes reviennent tout de suite, lues dans le fichier.
- Retirez des objets du coffre : la courbe du fer descend au relevé suivant. Cassez le coffre : les relevés suivants ont un champ vide, la courbe s'interrompt, et elle reprend quand le coffre revient.
- La rotation. Mettez
OCTETS_MAX = 200et laissez tourner : quand le journal dépasse 200 octets, il devientusine.ancien.csvet un nouveau journal commence avec son en-tête.ls /journauxmontre les deux fichiers.
Variantes
- D'autres valeurs. Tout ce qu'un périphérique lit peut être enregistré : le niveau d'un réservoir (
@tank.tanks,amountsurcapacity), les wagons comptés par une Boucle de détection (@inductive_loop.count), l'altitude d'un dirigeable (@altitude_sensor.height). Ajoutez un champ au relevé, une colonne à l'en-tête, un graphique àGRAPHES. - Minimum, maximum, moyenne. Parcourez
historiqueet écrivez les trois nombres sous le titre : la moyenne de la dernière heure en dit plus que la dernière valeur sur une chaîne qui tourne par à-coups. - Un compteur de jours.
os.day_timerevient à un petit nombre à chaque nouveau matin : retenez la valeur précédente et, quand la nouvelle est plus petite, ajoutez un à un compteur de jours sauvé dans un fichier. Les relevés se lisent alorsjour 12, 18:30. - Un enregistreur central. Envoyez chaque relevé avec
net.sendà un ordinateur de la salle de contrôle qui enregistre toutes les usines de la base ;Télécommande par le réseaumontre comment s'assurer que chaque message arrive. - Sur le tableau de bord. Le
Tableau de bord d'usinepeut montrer ces graphiques sur une seconde page.