Tutorial de Aurelpere | Catégories : Énergie
Building a photovoltaic tracker from a plate lifter
Building a photovoltaic tracker from a plate lifter
tracker, photovoltaique, phtv, dimensionnement
tracker:
BRD plate lifter: 150€
hydraulic cylinder "Actionneur linéaire 12V DC , 1320LBS(6000N) 20 pouces (500mm) moteur électrique" : 69€ on aliexpress, available on amazon a bit more expensive
Hbridge L298n 7a: 11€ delivered on aliexpress ("Moteur d'entrainement PWM 160W 7A 12V 24V, Module de commande L298, Signal de commande logique, optocoupleur, frein")
2 chain cogs 92T: "Pignon arrière 25H JO98/108/138 maillons 55T 65T 68T 70T 80T 92T, pour 47CC 49CC Mini Moto RL facades D343 Pit Pocket Bike" : 40€
2 cogs "Pignon pour scooter électrique 8T 9T 11T 13T 25H 410 420, pour moteur à courant continu 25H JOMotor MY1020 BM1109 MY1016Z MY1018"
10€
2 cogs "Pignon de moteur électrique pour Pit-Bike, pignon de moteur à courant continu, pièces RL, D343, 9T, 11T, 13T, 25H, JOMotor 25H"
4€
Engine "Moteur à engrenages CC à vis sans fin autobloquant, couple de bain, moteur de boîte de vitesses turbo en métal, inversé, basse vitesse, DC 12V, 24V, 200kg.cm" 62€
Module
Photovoltaic module Voltech 2mx1m 375W: 200€
Control:
raspberry: environ 100€
facom measuring tape: 20€
sovietic mass kit: 60€
spirit level: 5€
bracket: 6€
grinder: 50€
drill: 50€
welding machine: 100€
We set two axis for our module:
one axis Oz perpendicular to the module plane (vertical when the module is flat) and one axis Ox paralelle to the module plane (horizontal when the module is flat)
Caractéristics of the module:
mass: m=21,2 kg
Length: L=1,8m
Width: 1m
Wikipedia theory tells us intertia moment along Oz axis is :
Jdelta=1/12*m*L
Jdelta=1/12*1,8*21,2=3,18Nm
Nb: we suppose the module is a bar because here the rotation axis is the axis paralell to the plane of the rectangle formed by the module and not the perpendicular axis
We now experimentally verify:
Center the module on the plate lifter and put it horizontally
verify there is no wind
check the level on which the mast of the plate lifter is put
fix a horizontal lanmark at the bottom of the module
set a 500g mass at the extremity of the module
measure the distance between the position at equilibrium and the position with the mass
We have:
Jdelta=d*F
with d distance in meter to the axis of rotation
F force applied to the solid (here mg with m solid mass in kg and g gravitational constant)
We then have
Jdelta=0,9*0,5*9,8=4,41Nm
We measure the rotation: in our case, the distance of rotation at the extremity of the module varies between 3 and 10cm. The important variations
are due to frictions on the axis that can force or slide more freely
The order of magnitude of the inertia momen is verified
For the moment of inertia along axis Ox, it is more difficult to verify experimentally, because the axise of the plate lifter doesnt allow
to have a position at equilibrium with the module mass (which will finish on the stop under its own weight)
Therefore, we will accept the theory:
The theory says
Jdelta=1/12*m(b²+c²) with m mass of the module, b lenght of the short side of the module and c length of the long side of the module
Jdelta=1/12*21,2*(1,8²+1²)=7,49NM
This theoretical result is strongly lower than the weigth of the module, so we will size based on the necessary force to lift the weigth of the module (the axis being submitted to the weight):
F=mg=21,2*9,8
In order of magnitude 200Nm (1m of lever arm in order of magnitude)
We have now the caracteristics to size the engines of our module along two rotation axis
But the trackers have a consequent wind exposure
If we want to take into account the resistance to the wind, we must measure the force applied to the module according to wind speed:
Fp=1/2*ρ*v²*S*Cp
With ρ air density equal to 1,2 kg/m3 in order of magnitude for standard temperature and pressure conditions
v wind speed in m/s
S object surface in m²
Cp pressure coefficient without dimension égal to 2 for a rectangular metal plate
We have:
Fp=1/2*1,2*1,8*2*v²=2,16*v²
Chatgpt can give us the abacus of wind speeds in km/h and their conversions in m/2 and the generic name in meteorology
Calm: Less than 1 km/h (Less than 0.3 m/s)
Very light breeze: 1-5 km/h (0.3-1.5 m/s)
Light breeze: 6-11 km/h (1.6-3.0 m/s)
Gentle breeze: 12-19 km/h (3.4-5.4 m/s)
Moderate breeze: 20-28 km/h (5.5-7.9 m/s)
Fresh breeze: 29-38 km/h (8.0-10.7 m/s)
Strong breeze: 39-49 km/h (10.8-13.8 m/s)
Moderate wind: 50-61 km/h (13.9-16.9 m/s)
Near gale: 62-74 km/h (17.2-20.6 m/s)
Gale: 75-88 km/h (20.8-24.4 m/s)
Strong gale: 89-102 km/h (24.7-28.3 m/s)
Storm: 103-117 km/h (28.6-32.5 m/s)
Hurricane: At least 118 km/h (At least 32.8 m/s)
So we have a Force Fp that can vary from
An order of magnitude of 20N for a ligth breeze
to
An order of magnitude of 2000N for a storm
(NB: the gravity force of 1kg is approximately 10N so the force of 2000N corresponds in order of magnitude to the gravity force of 200kg).
The inertia moments on the axis are of the same order of magnitude (20Nm and 2000Nm) because the dimensions of the module are an order of magnitude of 1 m
If you want to build a tracker that can resist to storm conditions, it is advised to size the tracker consequently
On the one hand with sufficient ties in the ground, and on the other hand with an adapted frame -see caravan tips at the end of this stage-, and deactivate
when there are stronger winds.)
Wind resistance sizing is one of the reasons trackers are expensive et less generalized than standard photovoltaic installations
The step motors and servomotors that have an adequate torke to resist to important winds are expensive, et that can be understandable for motors designed for step precision
(For example here: https://www.distrelec.fr/fr/automatisation/moteurs-et-entrainements/moteurs-pas-pas-et-servocommandes/c/cat-L3D_525513 )
For hydraulic cylinders, the driving force is generally in orders of magnitude fitting to resist to storms.
For our low-tech tutorial, we know that core drilling machines have torque in an order of magnitude fitting to resist to storms (1W corresponds to 1 N multiplying 1 meter per second, so a core drilling machine of 2000W should have an adequate torque of more or less 2000Nm).
After verification on the technical caracteristics of drills, impact screwdriver, and after a manual verification of resistances (brake,clutch) when the engine is not powered, this type of engine doesnt fit.
To size a resistance to winds between the storm and hurricane we will proceed differently:
We find on aliexpress a affordable prices (70€) step motors or endless screw motors with torque in an order of magnitude of 20Nm(200kgcm). We will use thi engine with two reductors 1:10 to get a resisting torque of 2000Nm.
Recall: the torque expressed in Nm is a rotation force produced by the motor that can be computed with the same principle of the inertia moment: the force in newton x the distance to the rotation axis
Belt or chain transmission permit to reduce this torque, and that can be calculated very easily
C1=(R1/R2)*C2
With C1 torque on pulley 1
R1 pulley 1 radius
C2 torque on pulley 2
R2 pulley 2 radius
The radius is directly proportional to the number of teeth of a gear wheel (a bike cog or a motorcycle cog for example), we can easily calculate the transmission reduction by dividing the number of teeth of the big cog by the number of teeth of the little cog (verify they have the same teeth standards)
Therefore a transmission 80 teeth/10 teeth (80T/10T) will produce a reduction of 1:8, like here on amazon for an electric bike ("Keenso Kit Chaîne et Pignon 80T 25H 34mm 3 Trous Pignon 10T H Trou Chaîne Pignon 146 Maillons Chaîne") for 30€.
We will notice that a standard bike transmission have a maximum transmission reduction of 50 teeth/11 teeth so a reduction of 1:5, which is not enough for efficient reduction witouth multiplying pulleys
It is difficult to find transmissions with reduction of 1:10, but we find some on aliexpress and we will easily adapt a bicycle drive on which we will weld cogs, which will allow us a reduction of 1:100 with only one axis added in addition to the principal axis and the engine axis
So we will use an engine step motor or an endless screw engine (resistance unpowered test at reception) commanded by a raspberry pi (but the plate lifter has wheels, so we will take the precaution to put away the tracker when there's a storm ;))
Carvan tips: to size the width of the metal frame, modern norms dont seem to be really made on scientific basis but on commercial basis. We will therefore measure the width of old material (made in the 70s). For example, the frame of this old caravan accredited on its registration document for a 750kg weigth has a metal frame of 5mm width. On can then make a rule of 3 to verify the width of our frame fits.
This is rustic style (strenght of material calculs is rigorous and complex), but it allows to have a firest approximation "a visto de nas" as we say in gascon
Voir vidéos
Le moteur rotatif ("Moteur à engrenages CC à vis sans fin autobloquant, couple de bain, moteur de boîte de vitesses turbo en métal, inversé, basse vitesse, DC 12V, 24V, 200kg.cm" sur aliexpress) tient bien à 20Nmà vide hors tension et en rotation sous tension.
Rappel : torque ou couple = moment d'inertie engendré par le moteur
=force*distance à l'axe du moteur
ici 4kg à 0,5m=9,8*4*0,5=20Nm (en ordre de grandeur)
On va d'abord s'interesser à la trajectoire solaire ou solar trajectory en anglais.
On mesure typiquement la position du soleil selon deux systemes de coordonnées:
le systeme equatorial avec des coordonnées exprimées en:
ascension droite equivalente à la longitude terrestre mesurée en heures minutes secondes
déclinaison équivalente à la latitude terrestre mesurée en degré minutes secondes
le systeme horizontal avec des coordonnées exprimées en:
degré d'azimut
degré d'altitude ou de hauteur
Les abaques de trajectoire solaire (par exemple disponibles ici : https://www.astrolabe-science.fr/diagramme-solaire-azimut-hauteur ) nous donnent les trajectoires du soleil dans une journée (généralement plusieurs journées typiques de plusieurs saisons) exprimées en degrés horizontal.
Pour lire un graphique de ce type:
si on suit le graphique inséré dans ce tuto et issu du lien ci-dessus, lorsqu'on suit par exemple la courbe rouge pour paris, voici ce qu'on peut lire:
le 21 décembre, lorsqu'on regarde le sud, le soleil suit une trajectoire qui commence à -50° d'azimut (vers l'Est sur l'axe horizontal) lorsque le soleil se lève, puis lorsque le soleil va vers l'ouest tout au long de la journée (on suit la courbe rouge), il prend de la hauteur jusqu'à atteindre 17° de hauteur (sur l'axe vertical) à midi (position 0° d'azimut sur l'axe horizontal) puis redescend jusqu'à 0° de heuteur lorsqu'il se couche (vers l'Ouest sur l'axe horizontal).
Pour paris, on a :
un degré d'azimut qui varie de -130° à +130° selon l'heure et la saison
un degré d'altitude ou de hauteur qui varie de 0° à 64° selon l'heure et la saison
Pour notre tracker,
Pour calculer notre débattement horizontal (selon un axe Oz vertical si le module est posé au sol), on n'a pas vraiment de contrainte sur le lève plaque utilisé puisque l'axe tourne à 360° sans probleme. Donc on pourra suivre le soleil de -130° d'azimut à +130° d'azimut sans probleme.
Pour calculer notre debattement vertical (selon l'axe Ox horizontal si le module est posé au sol), on a une contrainte sur l'angle maximal.
N'ayant pas de décimetre sous la main, on va utiliser pythagore (voir photo):
64cm*150cm*134cm
socatoa:
sinus phi=opposé/hypothenus
sinus phi=134/150
sinus phi=0,8933
phi=1,1046 rad
phi=1,1046*180/pi=63°
On a donc une contrainte pour notre leve plaque qui accepte des angles selon l'axe Ox de 0° à 63°.
Lorsque le tracker est à son angle maximum (63°), on est perpendiculaire au soleil lorsque le soleil est à un angle phi de phi=180-63-90=27°
Lorsque le soleil a un angle plus faible que 27°, le tracker ne pourra pas suivre en étant perpendiculaire au soleil.
On voit cependant que la butée est assurée par le ressort (sur la photo on voit la marque au niveau de la peinture). On peut donc gagner en amplitude sur la butée en perçant et en faisant une encoche dans la potence.
La mesure manuelle du débattement entre l'axe du tube sur lequel est fixé la manivelle et le dos du module lorsque le leve plaque est incliné à son angle maximum nous donne 42cm. (voir photo)
On va fixer une potence sur la partie fixe du porte plaque qui tourne avec l'axe vertical, afin d'y fixer une tige sur laquelle on fixera le verin qui pourra tourner avec l'axe vertical afin d'ajuster l'angle sur l'axe horizontal.
On commence par fixer la potence en metal en la soudant à la partie fixe vis à vis de l'axe vertical. Il faut bien poncer la peinture avant de faire la soudure. (voir photo). On fait ici une soudure à l'arc.
On perce ensuite une tige en metal qu'on vient boulonner à la potence (voir photo).
On perce et on fixe également une tige en métal qu'on vient boulonner à la partie mobile qui ajuste l'angle sur l'axe horizontal, cad les "bras" qui permettent de porter la plaque ou le module photovoltaïque (voir photo).
On fixe ensuite le verin aux deux tiges en métal. Le verin est équipé de fixations avec des chevilles qui permettent de "suivre" l'angle pris par les tiges sur lesquelles il est fixé. (voir photo)
Remarquez qu'on a pris une tige en métal en angle droit afin d'éviter que cette fixation soit entièrement libre. Elle vient buter sur une partie de la tige, ce qui permet par la suite d'étaloner plus précisément l'amplitude du verin.
On teste ensuite la course du verin. Il faudra faire attention, car on arrive sur la butée du porte plaque à une avancée du verin d'environ 40cm et ce modèle a une course de 50cm. (voir photos)
Après observation des éléments bloquant, on va modifier la potence pour éviter la butée lorsque le soleil a un degré d'altitude inférieur à 27°.
Pour cela on va :
-laisser passer le ressort en évidant la potence (pour éviter que le ressort fasse butée)
-augmenter l'angle en abaissant la fixation du ressort en perçant la potence
-augmenter l'angle en coupant les bords de la potence
Voir photos :
1/2: observation dessus/dessous
3/4: demontage potence
4/5 : observation dessous, angle max à l'equerre
On arrive ainsi à un angle maximum de quasi 90° et on peut donc suivre le soleil sous tous les angles!
Pour controller le verin, on va utiliser un raspberry pi, l'ordinateur monocarte le plus répandu. Il est doté de d'une série 40 pins, qu'on peut connecter à divers appareils, appelés "controlleur GPIO".
Une première lecture des tutos et librairies disponibles pour utiliser le gpio et des un certain temps passer à tester des hypothèses éronnées pour installer correctement en utilisant les bonnes versions m'amène à vous exposer les options possibles:
-système d'exploitation :
Archives et versions de systemes d'exploitation utiles pour la rétrocompatibilité (tout lecteur soucieux de l'informatique low tech est incité à télécharger et garder des copies en local de ces archives et les partager en torrent!):
*dietpi:
https://dietpi.com/downloads/images/
*raspberry pi os:
https://downloads.raspberrypi.org/raspbian/images
si vous utilisez wheezy,
attention à bien mettre a jour votre /etc/apt/sources.list en faisant:
echo "deb http://legacy.raspbian.org/raspbian/ wheezy main contrib non-free rpi" >> /etc/apt/sources.list
NB: ChatGPT nous donne les dates suivantes de sortie des raspberry:
On espère que c'est vrai (ca vient de chatgpt), mais vous pouvez vérifier sur ce qui est écrit sur le pcb de votre carte.
Pour vérifier la version de votre raspberry sous raspberry pi os si vous ne savez pas quelle version cest(voir photo):
sudo usermod -a G gpio pi pinout
Ajustez avec le système d'exploitation qui vous parait le plus pertinent au regard des filtres de pertinence que vous utilisez. Vous avez la liste des anciennes versions d'os de dietpi et de raspberry pi os au cas où les versions les plus récentes ne fonctionneraient plus (et je me repette : tout lecteur soucieux de l'informatique low tech est incité à télécharger et garder des copies en local de ces archives et les partager en torrent!).
Pour l'install, comme d'habitude, telecharger balenaetcher, flasher une clé usb avec l'image téléchargée, booter. Les login/mdp par défaut pour dietpi sont root/dietpi et pour raspberry pi os pi/raspberry (attention au clavier en qwerty par défaut sous raspberry pi os au démmarage). Pour configurer le clavier, les locales, la timezone et le wifi, faire
sudo dietpi-configsous dietpi et
sudo raspi-configsous raspberry pi.
-driver utilisé pour controler le gpio
Essais infructueux avec pip et le depot pypi (erreur de compilations etc.). Installer en passant en root avec la commande
sudo -s
le plus simple est alors d'installer une version déjà compilée avec apt:
sudo apt install python3-rpi.gpio
faire un test pour voir si ca fonctionne bien (toujours en etant utilisateur root):
python3 import RPi.GPIO
Se reporter à la doc de sourceforge, plus explicite que celle de pypi: http://sourceforge.net/p/raspberry-gpio-python/wiki/Home/
installé par défaut sous raspberry pi
sous dietpi faire
sudo apt install python3 python3-venv python3-pip python3 -m venv venv source venv/bin/activate pip install lgpio gpiozero
Le premier point à comprendre est la numérotation des fiches: les pins (fiches) ont chacune un numéro qui va de 1 à 40 en suivant un ordre de bas en haut et de gauche à droite, et chaque pin a également un numéro GPIO qui est différent du numero de pin.
Pour cela on peut chercher les infos sur internet: https://wiki.lowtechlab.org/wiki/Serveur_orangepi-raspberry_nextcloud_en_photovolta%C3%AFque_autonome ou taper les commande suivantes dans raspberry pi os (voir image)
sudo usermod -aG gpio votre_user pinout
Dans ce tuto, que ce soit avec gpiozero ou RPi.GPIO, on utilisera la numerotation GPIO et pas la numérotation pin
On branche les GPIO 2 et 4 (alim +5V) aux deux fiches +5V du HBrdige On branche les GPIO 6 et 7 (Ground, la terre) aux deux fiches GND du HBrdige On branche les GPIO 23 et 16 à l'interrupteur 3 du HBridge Pour tester le "enable" du HBridge, on branche les GPIO 20 et 21 au ENA du HBridge
On utilise ensuite les scripts suivants :
import time import RPi.GPIO as GPIO import gpiozero def forwardzero(wait): led16=gpiozero.LED(16) #motor1 led23=gpiozero.LED(23) #motor1 led20=gpiozero.LED(20) #enable led21=gpiozero.LED(21) #enable led1=gpiozero.LED(1) led7=gpiozero.LED(7) try: print("forwardzero") #cas où le hbridge necessite un signal enable led21.off() led20.on() #signal à zero sur interupteur 2 led1.off() led7.off() #mettre un signal sur l'interrupteur 1 led23.on() led16.off() #laisser le signal actif le temps de wait for k in range(wait): print(k) time.sleep(1) #eteindre le signal sur l'interrupteur led23.off() led20.off() except KeyboardnInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") #signal à zero sur interupteur 2 led1.off() led7.off() #signal à zero sur l'interrupteur 1 led16.off() led23.off() #signal à zero sur la fiche enable led20.off() led21.off() def backwardzero(wait): led1=gpiozero.LED(1) led7=gpiozero.LED(7) led16=gpiozero.LED(16) #motor1 led23=gpiozero.LED(23) #motor1 led20=gpiozero.LED(20) #enable led21=gpiozero.LED(21) #enable try: print("backwardzero") #signal zero sur interupteur 1 led16.on() led23.off() #cas où le hbrige necessite un signal sur enable led20.off() led21.on() #signal sur interrupteur 2 led1.off() led7.off() #wait for k in range(wait): print(k) time.sleep(1) #signal off sur interrupteur led16.off() led21.off() except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") #signal à zero sur interupteur 1 led16.off() led23.off() #signal à zero sur l'interupteur 2 led1.off() led7.off() #signal à zero sur la fiche enable led20.off() led21.off() #pin16 gpio23 #pin36 gpio25 def forward(wait): GPIO.setmode(GPIO.BCM) GPIO.setup(16,GPIO.OUT) #motor1 GPIO.setup(23,GPIO.OUT) #motor1 GPIO.setup(20,GPIO.OUT) #enable GPIO.setup(21,GPIO.OUT) #enable GPIO.setup(1,GPIO.OUT) #motor2 GPIO.setup(7,GPIO.OUT) #motor2 try: print("forward") GPIO.output(1,GPIO.HIGH) GPIO.output(7,GPIO.LOW) GPIO.output(21,GPIO.HIGH) GPIO.output(20,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(16,GPIO.LOW) for k in range(wait): print(k) time.sleep(1) GPIO.output(1,GPIO.LOW) GPIO.output(21,GPIO.LOW) except KeyboardInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") GPIO.output(1,GPIO.LOW) GPIO.output(7,GPIO.LOW) GPIO.output(20,GPIO.LOW) GPIO.output(21,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(16,GPIO.LOW) #GPIO.cleanup() chez moi, ca n'enleve pas les +3V def backward(wait): GPIO.setmode(GPIO.BCM) GPIO.setup(20,GPIO.OUT) #enable GPIO.setup(21,GPIO.OUT) #enable GPIO.setup(1,GPIO.OUT) #motor2 GPIO.setup(7,GPIO.OUT) #motor2 GPIO.setup(16,GPIO.OUT) #motor1 GPIO.setup(23,GPIO.OUT) #motor1 try: print("backward") GPIO.output(21,GPIO.HIGH) GPIO.output(20,GPIO.LOW) GPIO.output(16,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(7,GPIO.HIGH) GPIO.output(1,GPIO.LOW) for k in range(wait): print(k) time.sleep(1) GPIO.output(7,GPIO.LOW) GPIO.output(21,GPIO.HIGH) except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") GPIO.output(20,GPIO.LOW) GPIO.output(21,GPIO.LOW) GPIO.output(16,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(1,GPIO.LOW) GPIO.output(7,GPIO.LOW) #GPIO.cleanup() chez moi ca n'enleve pas les +3V forward(2) #rotation horaire motor2 backward(2) #rotation antihoraire motor2 forwardzero(10) #verin extension motor1 111 max backwardzero(10) #verin retractation motor1 107 max
GPIO.BCM permet d'utiliser la numerotation GPIO GPIO.HIGH envoie un signal de +3V dans la fiche concernée GPIO.LOW remet le signal à 0V (GND, la terre) gpiozero fait la meme chose avec les methodes .on() et .off()
Update 31.05.24 : vous l'attendiez, voilà l'update du jour avec le hbridge made in europe, ca roule, voir vidéo! :) Update de l'étalonnage rapidement à reception d'un truc pour mesurer les angles un peu pratique.
Remarquez que les fiches 20 et 21 ne sont pas branchées au hbridge. Le "pwm" (pulse width modulation) n'est pas utilisé pour "activer" (enable dans le code) le moteur car des cavaliers placés sur les deux fiches ENA du hbridge suffisent (ici on n'a pas besoin de moduler la vitesse du moteur).
Fixer les poulies et la transmission 1:100 pour le moteur rotatif
On commence par récupérer deux pédaliers sur des vélos d'occasion à l'ébarbeuse en prenant soin de garder une tige du cadre.
On va venir y souder les pignons 92T (92 dents).
On soude ensuite les pignons 8T (8 dents) dessus.
On soude un pignon 8T sur un pignon avec une clavette qui s'adapte à l'axe du moteur.
On découpe une tige en fer et on soude un aplat le long de l'axe du lève plaque sur lequel on va venir fixer avec des boulons les tiges découpées dans les cadres de vélo qui prolongent le pédalier ainsi qu'une tige sur laquelle on va fixer le moteur.
On assemble et on boulonne.
Update à réceptiond de la 3eme chaine et en attendant de réfléchir à un moyen de régler les tensions de chaine.
Update du 11.6.24: les contraintes de l'axe du leve plaque font qu'on ne peut y fixer un grand pignon pour bénéficier d'un rapport de réduction favorable sur la derniere chaine de transmission. Malgré les réductions des transmissions des autres poulies, le lève plaque n'est pas entrainé en rotation (voir vidéo).
update du 16.6.24: réception du verin supplémentaire, date livraison estimée : 28 juin-2juillet
On va donc entrainer la rotation avec deux verins. Update à réception des verins fin juin (en stage et non dispo pour update ces prochaines semaines)
La tension de chaine etant mauvaise, on remplace les transmissions par un verrin hydraulique permettant de faire la rotation mais sur un angle réduit (environ 70° faute de verrin telescopique à plusieurs brins)
Le code mis à jour est le suivant: on définit des dictionnaire qui associe chaque angle recherché à un temps d'activation du moteur (à tester à la main et à mesurer)
#Etalonnage #dict_angle_rotation= sun_degre_azimut:motoractivationtime dict_angle_verin={1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:1, 12:1, 13:2, 14:3, 15:4, 16:5, 17:6, 18:7, 19:8, 20:9, 21:10, 22:11, 23:12, 24:13, 25:14, 26:15, 27:16, 28:16, 29:17, 30:18, 31:19, 32:20, 33:21, 34:22, 35:23, 36:24, 37:25, 38:27, 39:28, 40:29, 41:30, 42:32, 43:33, 44:34, 45:35, 46:37, 47:38, 48:39, 49:41, 50:42, 51:44, 52:45, 53:47, 54:48, 55:50, 56:51, 57:52, 58:54, 59:56, 60:58, 61:59, 62:59, 63:59, 64:60, 65:63, 66:64, 67:65, 68:66, 69:68, 70:68, 71:71, 72:73, 73:75, 74:77, 75:79, 76:81, 77:82, 78:83, 79:85, 80:86, 81:87, 82:89, 83:91, 84:94, 85:96, 86:98, 87:100} dict_angle_rotation={ 0:41, 1:42, 2:43, 3:44, 4:45, 5:46, 6:47, 7:48, 8:49, 9:50, 10:52, 11:53, 12:54, 13:55, 14:56, 15:57, 16:58, 17:59, 18:60, 19:61, 20:63, 21:64, 22:65, 23:66, 24:67, 25:68, 26:69, 27:70, 28:71, 29:72, 30:73, 31:75, 32:76, 33:77, 34:78, 35:79, 36:82, 37:82, 38:82, 39:82, 40:82, 41:82, 42:82, 43:82, 44:82, 45:82, 46:82, 47:82, -1:39, -2:37, -3:35, -4:33, -5:32, -6:30, -7:29, -8:27, -9:26, -10:25, -11:23, -12:22, -13:19, -14:18, -15:17, -16:16, -17:15, -18:14, -19:6, -20:2, -21:1, -22:0, }
Le code mis à jour est le suivant
import time import RPi.GPIO as GPIO import gpiozero import ephem import datetime def forwardzero(wait): led16=gpiozero.LED(16) #motor1 led23=gpiozero.LED(23) #motor1 led20=gpiozero.LED(20) #enable led21=gpiozero.LED(21) #enable led1=gpiozero.LED(1) led7=gpiozero.LED(7) try: print("forwardzero") #cas où le hbridge necessite un signal enable led21.on() led20.off() #signal à zero sur interupteur 2 led1.off() led7.off() #mettre un signal sur l'interrupteur 1 led23.on() led16.off() #laisser le signal actif le temps de wait for k in range(wait): print(k) time.sleep(1) #eteindre le signal sur l'interrupteur led23.off() led21.off() except KeyboardnInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") #signal à zero sur interupteur 2 led1.off() led7.off() #signal à zero sur l'interrupteur 1 led16.off() led23.off() #signal à zero sur la fiche enable led20.off() led21.off() def backwardzero(wait): led1=gpiozero.LED(1) led7=gpiozero.LED(7) led16=gpiozero.LED(16) #motor1 led23=gpiozero.LED(23) #motor1 led20=gpiozero.LED(20) #enable led21=gpiozero.LED(21) #enable try: print("backwardzero") #signal zero sur interupteur 1 led16.on() led23.off() #cas où le hbrige necessite un signal sur enable led20.off() led21.on() #signal sur interrupteur 2 led1.off() led7.off() #wait for k in range(wait): print(k) time.sleep(1) #signal off sur interrupteur led16.off() led21.off() except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") #signal à zero sur interupteur 1 led16.off() led23.off() #signal à zero sur l'interupteur 2 led1.off() led7.off() #signal à zero sur la fiche enable led20.off() led21.off() #pin16 gpio23 #pin36 gpio25 def forward(wait): GPIO.setmode(GPIO.BCM) GPIO.setup(16,GPIO.OUT) #motor1 GPIO.setup(23,GPIO.OUT) #motor1 GPIO.setup(20,GPIO.OUT) #enable GPIO.setup(21,GPIO.OUT) #enable GPIO.setup(1,GPIO.OUT) #motor2 GPIO.setup(7,GPIO.OUT) #motor2 try: print("forward") GPIO.output(1,GPIO.HIGH) GPIO.output(7,GPIO.LOW) GPIO.output(21,GPIO.HIGH) GPIO.output(20,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(16,GPIO.LOW) for k in range(wait): print(k) time.sleep(1) GPIO.output(1,GPIO.LOW) GPIO.output(21,GPIO.LOW) except KeyboardInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") GPIO.output(1,GPIO.LOW) GPIO.output(7,GPIO.LOW) GPIO.output(20,GPIO.LOW) GPIO.output(21,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(16,GPIO.LOW) #GPIO.cleanup() chez moi, ca n'enleve pas les +3V def backward(wait): GPIO.setmode(GPIO.BCM) GPIO.setup(20,GPIO.OUT) #enable GPIO.setup(21,GPIO.OUT) #enable GPIO.setup(1,GPIO.OUT) #motor2 GPIO.setup(7,GPIO.OUT) #motor2 GPIO.setup(16,GPIO.OUT) #motor1 GPIO.setup(23,GPIO.OUT) #motr1 try: print("backward") GPIO.output(21,GPIO.HIGH) GPIO.output(20,GPIO.LOW) GPIO.output(16,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(7,GPIO.HIGH) GPIO.output(1,GPIO.LOW) for k in range(wait): print(k) time.sleep(1) GPIO.output(7,GPIO.LOW) GPIO.output(21,GPIO.HIGH) except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") GPIO.output(20,GPIO.LOW) GPIO.output(21,GPIO.LOW) GPIO.output(16,GPIO.LOW) GPIO.output(23,GPIO.LOW) GPIO.output(1,GPIO.LOW) GPIO.output(7,GPIO.LOW) #GPIO.cleanup() chez moi ca n'enleve pas les +3V #forward(41) #rotation horaire motor2 #backward(90) #rotation antihoraire motor2 #forwardzero(10) #verin extension motor1 111 max #backwardzero(2) #verin retractation motor1 107 max #forwardzero(90) #Etalonnage #dict_angle_verin= sun_degre_horizontal:motoractivationtime #dict_angle_rotation= sun_degre_azimut:motoractivationtime dict_angle_verin={1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:1, 12:1, 13:2, 14:3, 15:4, 16:5, 17:6, 18:7, 19:8, 20:9, 21:10, 22:11, 23:12, 24:13, 25:14, 26:15, 27:16, 28:16, 29:17, 30:18, 31:19, 32:20, 33:21, 34:22, 35:23, 36:24, 37:25, 38:27, 39:28, 40:29, 41:30, 42:32, 43:33, 44:34, 45:35, 46:37, 47:38, 48:39, 49:41, 50:42, 51:44, 52:45, 53:47, 54:48, 55:50, 56:51, 57:52, 58:54, 59:56, 60:58, 61:59, 62:59, 63:59, 64:60, 65:63, 66:64, 67:65, 68:66, 69:68, 70:68, 71:71, 72:73, 73:75, 74:77, 75:79, 76:81, 77:82, 78:83, 79:85, 80:86, 81:87, 82:89, 83:91, 84:94, 85:96, 86:98, 87:100} dict_angle_rotation={ 0:41, 1:42, 2:43, 3:44, 4:45, 5:46, 6:47, 7:48, 8:49, 9:50, 10:52, 11:53, 12:54, 13:55, 14:56, 15:57, 16:58, 17:59, 18:60, 19:61, 20:63, 21:64, 22:65, 23:66, 24:67, 25:68, 26:69, 27:70, 28:71, 29:72, 30:73, 31:75, 32:76, 33:77, 34:78, 35:79, 36:82, 37:82, 38:82, 39:82, 40:82, 41:82, 42:82, 43:82, 44:82, 45:82, 46:82, 47:82, -1:39, -2:37, -3:35, -4:33, -5:32, -6:30, -7:29, -8:27, -9:26, -10:25, -11:23, -12:22, -13:19, -14:18, -15:17, -16:16, -17:15, -18:14, -19:6, -20:2, -21:1, -22:0, } def sun_position(time_now,lat,lon): now_here = ephem.Observer() now_here.lat = lat now_here.lon = lon #PyEphem only processes and returns dates that are in Universal Time (UT), which is simliar to Standard Time in Greenwich, England, on the Earth's Prime Meridian # Europe/Paris is GMT+2 #tester angles pyephem sur mesures réelles utc_now=datetime.datetime.utcnow() #is_dst=datetime.datetime(year=utc_now.year,month=utc_now.month,day=utc_now.day).dst() #time_diff=datetime.timedelta(hours=(1 if not is_dst else 2)) now_here.date = time_now #+datetime.timedelta(hours=time_diff) #'2007/10/02 00:50:22' sun.compute(now_here) sun_degre_azimut=int(sun.az*180/3.141592653589793) sun_degre_horizontal=int(sun.alt*180/3.141592653589793) return(sun_degre_horizontal,sun_degre_azimut) tracker_degré_horizontal=0 tracker_degré_azimut=0 def init(): global tracker_degré_horizontal forwardzero(111) tracker_degré_horizontal=0 def track(time_now,lat,lon): global tracker_degré_horizontal global tracker_degré_azimut init() (sun_degre_horizontal,sun_degre_azimut)=sun_position(time_now,lat,lon) sun_degre_azimut=min(sun_degre_azimut,87) sun_degre_horizontal=max(sun_degre_horizontal,-22) sun_degre_horizontal=min(36,sun_degre_horizontal) backwardzero(dict_angle_verin[sun_degre_horizontal]) tracker_degré_horizontal=sun_degre_horizontal backward(82) forward(dict_angle_rotation[sun_degre_azimut-tracker_degré_azimut]) tracker_degré_azimut=sun_degre_azimut #test Agen lat=44.2 lon=0.6 backward(4) forward(4) #placer le tracker direction sud angle horizontal backwardzero(4) forwardzero(41) #time.sleep(100) while True: track(datetime.datetime.now(),lat,lon) time.sleep(20*60) #activer le tracking toutes les 20 minutes
On va maintenant coder une "IA lowtech" pour le côté pédagogique (du ML pour machine learning, comme utilisé massivement depuis une quinzaine d'année dans de nombreux secteurs d'activité, cad pas l'ia au sens chatgptesque du terme en 2024). On pourrait dire que l'ia lowtech est l'ia dont les résultats ne relèvent pas de la pensée magique, dont les data et le code sont open source, non volés, dont les data sont à nous, par nous et pour nous et sur laquelle on a la main (ce dernier point est essentiel mais l'expérience de sortir du rang me pousse au pessimisme à ce sujet car la vérification s'il y a interférence ou pas dans les résultats du machine learning est assez difficile à détecter)
On branche une webcam, on enregistre les images comme données d'entrée, on traite l'image pour en faire un tableaux de chiffres correspondant à des variables avec lesquelles on va chercher à corréler avec un signal positif ou négatif (tourner le moter dans un sens ou moteur à l'arret).
Ici, les données d'entrées sont uniquement les 640*480*3=921600 variables des pixels des images de la vidéo (921600 colonnes/variables par lignes, à partir desquelles on cherche une corrélation avec le signal positif 1 ou 0 de la dernière colonne).
Pour faire fonctionner le tracker, ca ne fonctionnera pas bien, il faudrait faire du "feature engineering" (nom compliqué pour dire rajouter des colones de variables plus proabablement corrélées au signal positif) en rajoutant la luminosité et/ou la date et l'heure, sur des échantillons de vidéos couvrant toutes les saisons sur plusieurs années.
Si vous voulez apprendre les bases sur lesquelles reposent ce code, je recommande le cours "Applied Data Science with Python" de l'université du michigan dans lequelvous apprendrez des bases de python,pandas, et machine learning "no bullshit".
import cv2 import time import numpy as np import os import argparse import pandas as pd import matplotlib.pyplot as plt import numpy as np import seaborn as sns import sklearn.model_selection import sklearn.metrics import sklearn.decomposition from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.ensemble import RandomForestClassifier from sklearn.ensemble import GradientBoostingClassifier from sklearn.neural_network import MLPClassifier def enregistrer_video(out_file,fps,capture_duration): # Open the webcam cap = cv2.VideoCapture(0) # Use 0 for default webcam fourcc = cv2.VideoWriter_fourcc(*'XVID') # Codec (e.g., XVID) frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # Get webcam frame width frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # Get webcam frame height # Check if webcam opened successfully if not cap.isOpened(): print("Error opening webcam") exit() # Create the VideoWriter object out = cv2.VideoWriter(out_file, fourcc, fps, (frame_width, frame_height)) # Start time for tracking duration start_time = time.time() while time.time() - start_time < capture_duration: # Capture frame-by-frame ret, frame = cap.read() # Check if frame captured successfully if not ret: print("Error capturing frame") break # Write the frame to the video file out.write(frame) # Display the captured frame (optional) cv2.imshow('Webcam Video', frame) # Check if the user wants to quit (press 'q') if cv2.waitKey(1) & 0xFF == ord('q'): break # Close resources cap.release() out.release() cv2.destroyAllWindows() print(f"1 minute video saved successfully as {out_file}!") def charger_images_video(video_filename): """ Charge les images vidéo d'un fichier. Args: video_filename: Le chemin d'accès au fichier vidéo. Returns: Un tableau NumPy contenant les images vidéo (3D array: frames, rows, cols). """ # Ouvrir la vidéo avec OpenCV cap = cv2.VideoCapture(video_filename) # Vérifier l'ouverture réussie if not cap.isOpened(): print("Erreur d'ouverture du fichier vidéo:", video_filename) return None # Liste vide pour stocker les images vidéo images_list = [] # Lire les images vidéo image par image while True: ret, frame = cap.read() # Vérifier la lecture de l'image if not ret: break # Convertir l'image en nuance de gris (optionnel pour la normalisation) # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # Décommenter si nécessaire # Ajouter l'image à la liste images_list.append(frame) # Fermer la capture vidéo cap.release() return images_list def flatten_images_video(images_array): # Initier une liste de resultat result=[] # Iterer sur le numpy array en input for k in images_array: result.append(k.flatten()) # convertir la liste np.array result=np.asarray(result) return result # Exemple d'utilisation # Define video parameters out_file = "webcam_video_1min.mp4" # Output video filename fps = 20.0 # Frames per second capture_duration = 60 # Seconds #enregistrer_video(out_file,fps,capture_duration) images_video = charger_images_video("webcam_video_1min.mp4") # Fonction pour charger les images vidéo print(images_video) print(len(images_video)) # Flattening and normalizing images_video = flatten_images_video(images_video) print(images_video[0]) print(len(images_video[0])) # compression/normalisation scaling_factor = 1.0 / 255.0 # Divide by 255 to normalize between 0 and 1 images_video = images_video * scaling_factor print(images_video[0]) print(len(images_video[0])) # checking data shape #np.set_printoptions(threshold=np.inf) # Set threshold to infinity #print(np.array2string(images_video[0], suppress_small=True)) #print(len(images_video[0])) # Get the array shape image_shape = images_video.shape # Print the dimensions print("Image shape", image_shape) print("Number of dimensions:", len(image_shape)) print("Image height:", image_shape[0]) print("Image width:", image_shape[1]) print("Number of color channels:", image_shape[2]) # Total number of elements (height x width x color channels) total_elements = image_shape[0] * image_shape[1] * image_shape[2] print("Total elements:", total_elements)E # création du dataset pour dire "oui" pour tourner à gauche (par exemple) # ND: c'est à cette étape que les géants de la tech emploient des kenyans sous payés # dans une forme d'esclavage moderne # il s'agit de définir, pour chaque image, si on doit activer le moteur vers # la gauche (cad définir un signal positif pour que la machine fasse des corrélations # positives avec cette image) positives=np.zeros_like(images_video) #Si les 14 premieres images définissent un signal positif, on fera: #en réalité il faudra traiter des segments de vidéos positivement en définissant #chaque image auxquelles on va associer le signal positif positives[0:14] = 1 class ML(): @staticmethod def ml(X,y,classifier): "process machine learning on X data set, y yes/no data with classifier" # dataset #X = images_video #y = positives # classifier model training clf = ML.dico_classifier[classifier]() X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split( X, y, random_state=0) clf.fit(X_train, y_train) # predictions _predicted = clf.predict(X_test) # scores _accuracy, _precision, _recall = ML.compute_scores(y_test, _predicted, classifier) # confusion matrix ML.compute_confusion_matrix(y_test, _predicted, _accuracy, classifier) # courbes precision-recall ML.plot_precision_recall(clf, X_test, y_test, classifier, _predicted) # roc roc_auc_clf = ML.plot_roc(clf, X_test, y_test, classifier)[0] return pd.DataFrame(data=(_accuracy, _precision, _recall, roc_auc_clf), index=['accuracy', 'precision', 'recall', 'AUC'], columns=[classifier]) @staticmethod def compute_scores(y_test, _predicted, classifier): "compute machine learning scores" _accuracy = sklearn.metrics.accuracy_score(y_test, _predicted) _precision = sklearn.metrics.precision_score(y_test, _predicted) _recall = sklearn.metrics.recall_score(y_test, _predicted) print(str(classifier) + ' Accuracy: {:.2f}'.format(_accuracy)) print(str(classifier) + ' Precision: {:.2f}'.format(_precision)) print(str(classifier) + ' Recall: {:.2f}'.format(_recall)) return (_accuracy, _precision, _recall) @staticmethod def compute_confusion_matrix(y_test, _predicted, _accuracy, classifier): "compute confusion matrix" confusion_clf = sklearn.metrics.confusion_matrix(y_test, _predicted) df_clf = pd.DataFrame(confusion_clf, index=list(range(0, 2)), columns=list(range(0, 2))) plt.figure(figsize=(5.5, 4)) ax_heatmap=sns.heatmap(df_clf, annot=True, vmin=0, vmax=11, cmap="Blues") plt.title(str(classifier) + ' \nAccuracy:{0:.3f}'.format(_accuracy)) plt.ylabel('True label') plt.xlabel('Predicted label') return df_clf,ax_heatmap @staticmethod def plot_precision_recall(clf, X_test, y_test, classifier, _predicted): "plot precision recall curve" _precision = sklearn.metrics.precision_score(y_test, _predicted) _recall = sklearn.metrics.recall_score(y_test, _predicted) y_score_clf = clf.predict_proba(X_test) y_score_df = pd.DataFrame(data=y_score_clf) precision, recall, thresholds = sklearn.metrics.precision_recall_curve( y_test, y_score_df[1]) closest_zero = np.argmin(np.abs(thresholds)) closest_zero_p = precision[closest_zero] closest_zero_r = recall[closest_zero] plt.figure() plt.xlim([0.0, 1.01]) plt.ylim([0.0, 1.01]) result,=plt.plot(precision, recall) plt.title( str(classifier) + ' Precision-Recall Curve \nprecision :{:0.2f}'.format(_precision) + ' recall: {:0.2f}'.format(_recall)) plt.plot(closest_zero_p, closest_zero_r, 'o', markersize=12, fillstyle='none', c='r', mew=3) plt.xlabel('Precision', fontsize=16) plt.ylabel('Recall', fontsize=16) plt.show() return result @staticmethod def plot_roc(clf, X_test, y_test, classifier): "plot roc curve" y_score_clf = clf.predict_proba(X_test) y_score_df = pd.DataFrame(data=y_score_clf) fpr_clf, tpr_clf, _ = sklearn.metrics.roc_curve(y_test, y_score_df[1]) roc_auc_clf = sklearn.metrics.auc(fpr_clf, tpr_clf) plt.figure() plt.xlim([-0.01, 1.00]) plt.ylim([-0.01, 1.01]) result,=plt.plot(fpr_clf, tpr_clf, lw=3, label=str(classifier) + ' ROC curve (area = {:0.2f})'.format(roc_auc_clf)) plt.xlabel('False Positive Rate', fontsize=16) plt.ylabel('True Positive Rate', fontsize=16) plt.title('ROC curve ' + str(classifier) + ' \nAUC:{0:.3f}'.format(roc_auc_clf), fontsize=16) plt.legend(loc='lower right', fontsize=13) plt.plot([0, 1], [0, 1], color='navy', lw=3, linestyle='--') plt.show() return roc_auc_clf,result dico_classifier = { 'knn': KNeighborsClassifier, 'naiveb': GaussianNB, 'randomforest': RandomForestClassifier, 'gtree': GradientBoostingClassifier, 'neural': MLPClassifier} @staticmethod def plot_heatmap(dataframe): "plot heatmap of accuracy, precision, recall, AUC" plt.figure() sns.heatmap(dataframe, annot=True, vmin=0, vmax=1, cmap="Blues") plt.title('scores des classifiers ') plt.ylabel('scores') plt.xlabel('modeles') plt.show() #process machine learning for all classifiers in dico_classifier #and plot a heatmap of their accuracy, precision, recall, AUC df_result = pd.DataFrame(data=(0, 0, 0, 0), columns=['init'], index=['accuracy', 'precision', 'recall', 'AUC']) for clf in ML.dico_classifier: print(clf) result_ml = ML.ml(images_video, positives, clf) df_result = pd.merge(df_result, result_ml, right_index=True, left_index=True) df_result.drop('init', axis=1, inplace=True) ML.plot_heatmap(df_result) return df_result
Conclusion: Voilà, maintenant que vous savez coder une IA, vous pouvez la critiquer d'autant mieux,et promouvoir les lowtech en connaissance de cause.
Vous noterez que les algorithmes d'ia sont open sources et assez faciles à utiliser en tant que développeur "simple utilisateur".
Et aussi que sans data, l'ia ne sert absolument à rien.
C'est pour cette raison que les géants de la tech veulent toujours plus de données et emploient des gens dans des conditions proches de l'esclavage dans de nombreux pays pour les traiter avant d'entrainer leurs modèles.
Tout comme pour les "données personnelles", la question clé des ia repose sur les données.
Voir l'excellente conf de benjamin bayart " Géopolitique de la data (Benjamin BAYART) " sur youtube ou en vidéo ici.
Vous pouvez aussi faire un tracker lowtech, mais aussi adapter le code pour créer un véhicule autonome lowtech avec 4 datasets/signaux positifs distincts pour entrainer l'activation de "tourner à gauche","accélérer", "tourner à droite", "freiner". C'est ce qu'a fait George Hotz en proclamant qu'il suffisait d'une trentaine d'heures de vidéos de conduite en enregistrant avec des capteurs pour avoir les signaux positifs correspondant aux images enregistrées pour que le machine learning fonctionne.
Evidemment, on espere qu'il n'y aura pas de hack ou que le système n'a pas de controle commande à distance sur ce type d'algorithme.
en fr 1 Published
Vous avez entré un nom de page invalide, avec un ou plusieurs caractères suivants :
< > @ ~ : * € £ ` + = / \ | [ ] { } ; ? #