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
See videos
The rotative 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" on aliexpress) takes well 20Nm being powered and not being powered.
Recall: torque = moment of intertia generated by the motor
=force*distance to motor axis
here 4kg at 0,5m=9,8*4*0,5=20Nm (in order of magnitude)
We will first look at the solar trajectory
We measure typically the sun position along two coordinate systems:
the equatorial system with coordinates expressed in:
right ascension equivalent to terrestrial longitude measured in hours minutes seconds
declination equivalent to terrestrial latitude measured in degrees minutes seconds
The horizontal system with coordinates expressed in:
azimut degree
altitude degree or height
The solar trajectory abacus (for example available here https://www.astrolabe-science.fr/diagramme-solaire-azimut-hauteur ) give us the sun trajectories in a day (generally several typical days of several seasons) expressed in horizontal degrees.
To read this type of grap
If we follow the graph inserted in this tutorial and from the link above, when we follow for example the red curb for paris, we can read :
The 21st of december, when we look at the south, the sun follows a trajectory that starts at -50° azimut (along the East on the horizontal axis) at dawn, then when the sun goes west during the day (we follow the red curb), it takes height until it reaches 17° height (on the vertical axis) at noon (position 0° azimut on the horizontal axis) and then goes down again to 0° height when it sets (along the West on the horizontal axis)
For Paris, we have:
One degree azimut varying from -130° to +130° according to the hour and the season
One degree altitude or height varying from 0° to 64° according to the hour and the season
For our tracker,
To calculate our horizontal displacement (along the Oz vertical axis if the module is put down on the ground), on don't really have constraints on the plate lifter used since the axis turns 360° without problems.So we can follow the sun from -130° azimut to +130° azimut without problem.
To calculate our vertical displacement (along the Ox horizontal axis if the module is put on the ground), we have a constraint on the maximal angle.
I dont have a decimeter at hand, so I will use pythagore (see photo):
64cm*150cm*134cm
socatoa:
sinus phi=opposé/hypothenus
sinus phi=134/150
sinus phi=134/150
phi=1,1046 rad
phi=1,1046*180/pi=63°
We have a constrainte for our plate lifter accepting angles along the Ox axis from 0° to 63°
When the tracker is at its maximum angle (63°), we are perpendicular to the sun when the sun is at an angle phi of phi=180-63-90=27°
When the sun has an angle lower than 27°, the tracker can not follow and keep being perpendicular to the sun
We see however that the stop is guaranteed by the spring (on the photo we sse the mark on the paint). We can win a bit of amplitude on the stop by drilling and making a notch in the jib).
The manual measurement of the displacement between the tube axis on which is fixed the crank handle and the back of the module when the plate lifter leans at its maximum angle gives us 42cm. (see photo)
We will fix a jib on the fix part of the plate lifter that turns with the vertical axis, so we can fix a rod on which we will fix the hydraulic cylinder which will be able to turn with the vertical axis so it can adjust the angle on the horizontal axis.
We begin by fixing the metal jib welding to the fix part regarding the vertical axis. It requires to sand the paint before the welding is done (see photo). We do arc welding here.
Then we drill a metal rod we will screw to the jib (see photo).
We drill et we fix a metal rod we screw to the mobile part that allows to adjust the angle on the horizontal axis, ie the arm that permits to carry the plate or the photovoltaic module (see photo).
We then fix the hydraulic cylinder to the two metal rods. The hydraulic cylinder is equiped with fixing that adjust to the angle taken by the rods on which it is fixed (see photo)
Notice we have a metal rod with a perpendicular angle that avoid this fix to be totally free. It will stop on a part of the rod, which permits then to calibrate more precisely the hydraulic cylinder amplitude).
We then test the course of the hydraulic cylinder. We will pay attention, because the stop of the plate lifter corresponds to a 40cm course for the hyrdaulic cylinder and it can extend up to 50cm. (see photos)
After observing the critical elements, we will modify the jib to avoid a stop when the sun has a altitude degree lower than 27°.
To do so, we will:
-let the spring go by scooping the jib (to avoid the spring be a stop)
-raise the angle by lowering the fix of the spring drilling the jib
-raise the angle cutting the edges of the jib
See photos:
1/2: observation top/below
3/4: jib disassembly
4/5: obersvation below, maximum angle at square
We reach an maximum angle of almost 90° and we can completely follow the sun!
To control the hydraulic cylinder, we will use a rapsberry pi, the most widespread monocard computer. It is equiped with a serie of 40 pins, we can connect devices to, called GPIO controller.
A first reading of a few tutorials and available libraries to use it a some time taken to test wrong hypothesis to install correctly using the good versions leads me to talk a few possible options:
-operating system:
Archives and operating systems versions usefull for retrocompatibility (every reader caring for low tech computer is encouraged to keep local copies of these archives and to share them in torrent!):
*dietpi:
https://dietpi.com/downloads/images/
*raspberry pi os:
https://downloads.raspberrypi.org/raspbian/images
si vous utilisez wheezy,
echo "deb http://legacy.raspbian.org/raspbian/ wheezy main contrib non-free rpi" >> /etc/apt/sources.list
NB: ChatGPT gives us the following rapsberry release dates:
We hope this is true (it comes from chatgpt), but you can verify on what's written on the pcb of your card.
To verify the version of your raspberry under raspberry pi os if you dont know what version it is (see photo):
sudo usermod -a G gpio pi pinout
Adjust with the operating system that you think is relevant based on your relevancy filters. You have a list of old os versions of dietpi and raspberry pi os in case new versions would now ork anymore (and i repeat: every reader caring of low tech computer is encouraged to download and keep local copies of these archives and share them in torrent!)
To install, as usually, download balenaetcher, flash a usb key with the donwloaded image, boot. The default login/password on dietpi are root/dietpi and for raspberry pi os pi/raspberry (take care to the default qwerty on raspberry pi os at boot). To configure keyboard, locales, timezone and wifi, do:
sudo dietpi-configunder dietpi and
sudo raspi-configunder raspberry pi.
-driver used to control the gpio
Failure with pip and pypi repository (compilation errors etc.). Install as root with command
sudo -s
The simpler is to install a precompiled version with apt:
sudo apt install python3-rpi.gpio
do a test to see if that works well (as root):
python3 import RPi.GPIO
See the sourceforge documentation, more explicit than the pypi one: http://sourceforge.net/p/raspberry-gpio-python/wiki/Home/
under dietpi do
sudo apt install python3 python3-venv python3-pip python3 -m venv venv source venv/bin/activate pip install lgpio gpiozero
or
sudo apt install python3-gpiozero
The first point to understand is the numbering of the pins: The pins have all a number that goes from 1 to 40 following an order from bottom to top and from left to right, and each pin has also a gpiio number which is different from the pin number.
To do so, we can find information on internet: https://wiki.lowtechlab.org/wiki/Serveur_orangepi-raspberry_nextcloud_en_photovolta%C3%AFque_autonome or type the command under raspberry pi os (see image)
sudo usermod -aG gpio votre_user pinout
In this tutorial, wether for gpiozero or RPi.GPIO, we will use the GPIO numbering and not the pin numbering
We plug the GPIO 2 and 4 (+5V) to the +5V pins of the HBridge We plug the GPIO 6 and 7 (Ground) to the GND pin of the HBridge We plug the GPIO 23 and 16 to the switch 3 of the HBridge To test the "enable" of the HBridge, we plug the GPIO 20 and 21 to EAN of HBridge
We then use the following scripts:
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") #case where Hbridge needs a enable signal led21.off() led20.on() #zero signal off on switch 2 led1.off() led7.off() #positive signal off on switch 1 led23.on() led16.off() #let signal active during wait time for k in range(wait): print(k) time.sleep(1) #put signal off on the switch led23.off() led20.off() except KeyboardnInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") #zero signal off on switch 2 led1.off() led7.off() #signal off on switch 1 led16.off() led23.off() #signal off on enable pin 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 off on switch 1 led16.on() led23.off() #case where Hbridge needs a enable signal led20.off() led21.on() #signal on switch 2 led1.off() led7.off() #wait for k in range(wait): print(k) time.sleep(1) #signal off on switches led16.off() led21.off() except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") #signal off on switch 1 led16.off() led23.off() #signal off on switch 2 led1.off() led7.off() #signal off on enable pin 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() on my sbc it doesnt remove the +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() on my sbc it doesnt remove the +3V forward(2) #rotation horary motor2 backward(2) #rotation antihorary motor2 forwardzero(10) #hydraulic cylinder extension motor1 111 max backwardzero(10) #hydraulic cylinder retractation motor1 107 max
GPIO.BCM allows to use GPIO numbering GPIO.HIGH sends a +3V signal in the concerned pin GPIO.LOW sets the signal to 0V (GND) gpiozero does the same with .on() and .off() methods
Update 31.05.24: you were waiting for it, here's today update with the HBridge made in Europe. It works, see video! Update of calibration quickly at recepetion of something to measure angles a bit more handy.
Notice the pins 20 and 21 are not plugged to the HBridge. The "PWM" (pulse width modulation) is not used to "activate" (enable in the code) the engine because jumbers on ENA pins of the Hbridge are enough (here we dont need to adjust motor speed)
Fix pulleys and 1:100 transmission for rotation motor
We begin by recycling two bycicle drives from second hand bikes we cut with a grinder taking care to keep the metal rod of the frame.
we will weld 92T cogs on it.
We then weld the 8T cogs on it.
We weld a 8T cog on a locked cog adapted to the motor axis.
We cut a metal rod and we weld a flat metal plate along the axis of the plate lifter on which we will screw the bike metal rods attached to the bike drives and alos a metal rod on which we will fix the motor.
We assemble and we screw.
Update when receiving the 3rd chain and waiting how to fix the chain tensions.
Update of 11.6.24: the constraints of the axis of the plate lifter make it difficult to fix a big cog to get a good reduction on the last transmission chain. Even we have several reductions with other pulleys, the plate lifter is not rotating (see video).
update of 16.6.24: additional hydraulic cylinder ordered. Estimated delivery 28 juin-2juillet
We will make the rotation with two hydraulic cylinders. Update when received at the end of june (not available these next weeks anyway).
The chain tension is bad, we replace the tranmissions with a hydraulic cylinder allowing a rotation but with a reduced angle (about 70° because it is not a 2 or 3 parts telescopic cylinder)
The updated code is as follow: we define dictionnaries associating each sought angle to an activation time of the engine (each must be manually tested and measured)
#Calibration #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, }
The updated code is as follows:
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") #case where Hbridge needs a enable signal led21.off() led20.on() #zero signal off on switch 2 led1.off() led7.off() #positive signal off on switch 1 led23.on() led16.off() #let signal active during wait time for k in range(wait): print(k) time.sleep(1) #put signal off on the switch led23.off() led20.off() except KeyboardnInterrupt: print("keyboard interrupt") except Exception as err: print(err) finally: print("zero") #zero signal off on switch 2 led1.off() led7.off() #signal off on switch 1 led16.off() led23.off() #signal off on enable pin 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 off on switch 1 led16.on() led23.off() #case where Hbridge needs a enable signal led20.off() led21.on() #signal on switch 2 led1.off() led7.off() #wait for k in range(wait): print(k) time.sleep(1) #signal off on switches led16.off() led21.off() except KeyboardInterrupt: print("keyboardinterrupt") except Exception as err: print(err) finally: print("zero") #signal off on switch 1 led16.off() led23.off() #signal off on switch 2 led1.off() led7.off() #signal off on enable pin 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() on my sbc it doesnt remove the +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() on my sbc it doesnt remove the +3V #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 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
We will now code a lowtech AI for the educational part (Machine Learning here, as massively used since about fifteen years in many industries, ie no AI in the chatgpt sense as it is usually understood in 2024). We could say lowtech AI is AI with results not being magic, where data and code are open source, not stolen, where data belong to us, by us and for us, and on which we are in control of play (this last point is essential but the experience to step out of the line drives me to pessimism on this matter because testing if there is interference in an machine learning result is difficult to detect)
We plug a webcam, we record the images as input data, we process the image to make a digit table corresponding to variables with which on wee seek to correlate with a positive or negative signal (turn the engine in one direction, or stop)
Here, the input data are only the 640*480*3=921600 variables of pixels of the images in the video (921600 colunns/variables per line, from which we seek to correlate with a positive signal 1 or 0 of the last column).
To have the tracker work, this method will not fits well, it would require to do "feature engineering" (complicated name to say add columns of variables more probably correlated to the positive signal) by adding luminosity and/or date and time, on video samples covering all seasons on many years.
If you want to learn the basics of AI on which this code relies, I recommand the course "Applied Data Science with Python" of michigan university in which you will learn python, pandas and machine learning basics "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): """ Load video images from a file Args: video_filename: Le chemin d'accès au fichier vidéo. Returns: a numpy table with video images (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) # dataset creation to say "yes" to turn left (for example) # NB: it's at this stage that tech giants employ kenyans at very low wages # in a form of modern slavery # it's about defining, for each image, if we must activate the motor # on the loeft (ie defining a positive signal for the machine to correlate positively with this image) positives=np.zeros_like(images_video) #If first 14 images define a positive signal, we will do: #in reality it would require to process video intervals positively defining #for each image those where we associate a positive signal 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:
Here, now you know how to code using AI, you can critize it better et promote low tech adequately
You will notice AI algorithms are open source and easy to use as simple "dev user".
And also that without data, AI is absolutely useless.
That's the reason why big tech giants want more and more data and employ peolple in slavery conditions in many countries to process these data before training their models.
Just like for "personal data", the key question here with AI relies on data.
See the excellent conference of Benjamin Bayart "Geopolitique de la data (Benjamin BAYART)" on youtube or in video in this tutorial.
You can make a lowtech tracker, but also adapt this code to create an autonomous vehicle lowtech with 4 distinct datasets/positive signals to train activation of "turn left", "accelerate", "brake", "turn right". This is what George Hotz did claiming it required only 30 hours of videos of driving and recorded sensors to have the needed positives signals correlated with the recorded images to have autonomous driving work.
Of course, we strongly hope there won't be hack or that the system doesnt have a distant command and control on this type of algorithm.
en fr 1 Published
Vous avez entré un nom de page invalide, avec un ou plusieurs caractères suivants :
< > @ ~ : * € £ ` + = / \ | [ ] { } ; ? #