Sadržaj:
Video: Robotsko sortiranje perlica: 3 koraka (sa slikama)
2024 Autor: John Day | [email protected]. Zadnja promjena: 2024-01-30 09:36
U ovom ćemo projektu izgraditi robota za sortiranje Perlerovih perlica po boji.
Oduvijek sam želio izgraditi robota za sortiranje boja, pa kad se moja kći zainteresirala za izradu Perlerovih perlica, vidio sam to kao savršenu priliku.
Perlerove perle koriste se za stvaranje spojenih umjetničkih projekata postavljanjem mnogih perli na ploču, a zatim njihovo topljenje peglom. Općenito, kupujete ove perle u ogromnim pakiranjima od 22 000 perlica u različitim bojama i trošite puno vremena na traženje boje koju želite, pa sam mislio da će njihovo razvrstavanje povećati umjetničku učinkovitost.
Radim za Phidgets Inc. pa sam za ovaj projekt uglavnom koristio Phidgets - ali to se može učiniti pomoću bilo kojeg prikladnog hardvera.
Korak 1: Hardver
Evo što sam koristio za izgradnju ovoga. Napravio sam ga 100% s dijelovima s phidgets.com i stvarima koje sam imao po kući.
Ploče Phidgets, Motori, Hardver
- HUB0000 - VINT Hub Phidget
- 1108 - Magnetski senzor
- 2x STC1001 - 2.5A Stepper Phidget
- 2x 3324 - 42STH38 NEMA -17 Bipolarni stepenišni mjenjač
- 3x 3002 - Phidget kabel 60 cm
- 3403 - USB2.0 4 -portno čvorište
- 3031 - Ženski pigtail 5.5x2.1mm
- 3029 - 2 žica 100 'upleteni kabel
- 3604 - 10 mm bijela LED (vrećica od 10)
- 3402 - USB web kamera
Ostali dijelovi
- Napajanje 24VDC 2.0A
- Otpadno drvo i metal iz garaže
- Zip kravate
- Plastična posuda s odrezanim dnom
Korak 2: Dizajnirajte robota
Moramo dizajnirati nešto što može uzeti jednu kuglicu iz ulaznog lijevka, staviti je pod web kameru, a zatim je premjestiti u odgovarajuću posudu.
Preuzimanje perli
Odlučio sam napraviti prvi dio s 2 komada okrugle šperploče, svaki s rupom izbušenom na istom mjestu. Donji dio je fiksiran, a gornji dio je pričvršćen na koračni motor, koji ga može rotirati ispod lijevka napunjenog kuglicama. Kad rupa prođe ispod lijevka, uhvati jednu kuglicu. Zatim ga mogu okretati ispod web kamere, a zatim dalje rotirati dok se ne podudara s rupom na donjem dijelu, u kojem trenutku propada.
Na ovoj slici testiram da li sustav može raditi. Sve je fiksirano osim gornjeg okruglog komada šperploče, koji je pričvršćen na koračni motor ispod pogleda. Web kamera još nije montirana. Upravo koristim upravljačku ploču Phidget za okretanje motora u ovom trenutku.
Skladištenje perli
Sljedeći dio je projektiranje sustava kante za držanje svake boje. Odlučio sam upotrijebiti drugi koračni motor ispod za podršku i zakretanje okruglog spremnika s ravnomjerno raspoređenim odjeljcima. To se može koristiti za rotiranje ispravnog odjeljka ispod rupe iz koje će zrnca ispasti.
Napravio sam ovo koristeći karton i ljepljivu traku. Najvažnija stvar ovdje je dosljednost - svaki odjeljak trebao bi biti iste veličine, a cijela stvar treba biti ravnomjerno ponderirana tako da se vrti bez preskakanja.
Uklanjanje perlica postiže se pomoću čvrsto zatvorenog poklopca koji izlaže jedan pretinac odjednom, tako da se perle mogu izliti.
Fotoaparat
Web kamera je postavljena preko gornje ploče između lijevka i mjesta rupe donje ploče. To omogućuje sustavu da pogleda zrnce prije nego što ga ispusti. LED se koristi za osvjetljavanje perlica ispod kamere, a ambijentalno svjetlo je blokirano kako bi se osiguralo dosljedno svjetlosno okruženje. To je vrlo važno za precizno otkrivanje boja, jer ambijentalno osvjetljenje zaista može odbaciti uočenu boju.
Otkrivanje lokacije
Važno je da sustav može otkriti rotaciju separatora kuglica. To se koristi za postavljanje početnog položaja pri pokretanju, ali i za otkrivanje je li koračni motor sinkroniziran. U mom sustavu, zrnca će se ponekad zaglaviti dok ih se podigne, a sustav je morao biti u stanju otkriti i riješiti ovu situaciju - tako da se malo sigurnosno kopira i pokuša.
Postoji mnogo načina za rješavanje ovoga. Odlučio sam koristiti magnetski senzor 1108, s magnetom ugrađenim u rub gornje ploče. To mi omogućuje provjeru položaja pri svakoj rotaciji. Bolje rješenje vjerojatno bi bio koder na koračnom motoru, ali imao sam 1108 koji je ležao pa sam to upotrijebio.
Dovrši robota
U ovom trenutku sve je razrađeno i testirano. Vrijeme je da sve lijepo montirate i prijeđete na softver za pisanje.
Dva koračna motora pokreću stepenasti kontroleri STC1001. HUB000 - USB VINT čvorište koristi se za pokretanje stepper kontrolera, kao i za čitanje magnetskog senzora i pogon LED -a. Web kamera i HUB0000 su spojeni na mali USB hub. Za napajanje motora koristi se 3031 pigtail i nešto žice zajedno s napajanjem od 24V.
Korak 3: Napišite kôd
Za ovaj projekt koriste se C# i Visual Studio 2015. Preuzmite izvor na vrhu ove stranice i slijedite ga dalje - glavni odjeljci navedeni su u nastavku
Inicijalizacija
Prvo moramo stvoriti, otvoriti i inicijalizirati objekte Phidget. To se radi u događaju učitavanja obrasca i rukovateljima za dodavanje Phidget -a.
private void Form1_Load (pošiljatelj objekta, EventArgs e) {
/ * Pokretanje i otvaranje Phidgeta */
top. HubPort = 0; top. Priključivanje += Gornje_priključivanje; top. Detach += Top_Detach; top. PositionChange += Top_PositionChange; top. Open ();
bottom. HubPort = 1;
bottom. Attach += Bottom_Attach; odozdo. Detach += Bottom_Detach; bottom. PositionChange += Bottom_PositionChange; dno. Open ();
magSensor. HubPort = 2;
magSensor. IsHubPortDevice = true; magSensor. Attach += MagSensor_Attach; magSensor. Detach += MagSensor_Detach; magSensor. SensorChange += MagSensor_SensorChange; magSensor. Open ();
led. HubPort = 5;
led. IsHubPortDevice = istina; led. Kanal = 0; led. Pričvrstite += Led_Attach; vodio. Detach += Led_Detach; led. Open (); }
private void Led_Attach (pošiljatelj objekta, Phidget22. Events. AttachEventArgs e) {
ledAttachedChk. Checked = true; led. Država = istina; ledChk. Checked = istina; }
private void MagSensor_Attach (pošiljatelj objekta, Phidget22. Events. AttachEventArgs e) {
magSensorAttachedChk. Checked = true; magSensor. SensorType = VoltageRatioSensorType. PN_1108; magSensor. DataInterval = 16; }
private void Bottom_Attach (pošiljatelj objekta, Phidget22. Events. AttachEventArgs e) {
bottomAttachedChk. Checked = true; bottom. CurrentLimit = bottomCurrentLimit; bottom. Engaged = true; bottom. VelocityLimit = bottomVelocityLimit; bottom. Acceleration = bottomAccel; bottom. DataInterval = 100; }
private void Top_Attach (pošiljatelj objekta, Phidget22. Events. AttachEventArgs e) {
topAttachedChk. Checked = true; vrh. Ograničenje struje = Ograničenje vrhunske struje; top. Angaged = true; top. RescaleFactor = -1; top. VelocityLimit = -topVelocityLimit; top. Acceleration = -topAccel; top. DataInterval = 100; }
Tijekom inicijalizacije čitamo i sve spremljene podatke o boji, tako da se prethodni rad može nastaviti.
Pozicioniranje motora
Kôd za rukovanje motorom sastoji se od praktičnih funkcija za pomicanje motora. Motori koje sam koristio su 3, 200 1/16 koraka po okretu, pa sam za to stvorio konstantu.
Za gornji motor postoje 3 pozicije koje želimo poslati motoru na: web kameru, rupu i magnet za pozicioniranje. Postoji funkcija za putovanje do svakog od ovih položaja:
private void nextMagnet (Boolean wait = false) {
double posn = top. Position % stepsPerRev;
top. TargetPosition += (stepsPerRev - posn);
ako (čekaj)
while (top. IsMoving) Thread. Sleep (50); }
private void nextCamera (Boolean wait = false) {
double posn = top. Position % stepsPerRev; if (posn <Properties. Settings. Default.cameraOffset) top. TargetPosition += (Properties. Settings. Default.cameraOffset - posn); else top. TargetPosition + = ((Properties. Settings. Default.cameraOffset - posn) + stepsPerRev);
ako (čekaj)
while (top. IsMoving) Thread. Sleep (50); }
private void nextHole (Boolean wait = false) {
double posn = top. Position % stepsPerRev; if (posn <Properties. Settings. Default.holeOffset) top. TargetPosition += (Properties. Settings. Default.holeOffset - posn); else top. TargetPosition + = ((Properties. Settings. Default.holeOffset - posn) + stepsPerRev);
ako (čekaj)
while (top. IsMoving) Thread. Sleep (50); }
Prije početka trčanja gornja se ploča poravnava pomoću magnetskog senzora. Funkcija alignMotor može se pozvati u bilo kojem trenutku radi poravnavanja gornje ploče. Ova funkcija prvo brzo okreće ploču do 1 punog okreta dok ne vidi podatke magneta iznad praga. Zatim se malo sigurnosno kopira i polako se ponovno pomiče prema naprijed, bilježeći podatke senzora. Konačno, postavlja položaj na maksimalno magnetsko mjesto podataka i poništava položaj pomaka na 0. Dakle, maksimalni položaj magneta uvijek treba biti na (vrh. Pozicija % stepsPerRev)
Niz alignMotorThread; Boolean sawMagnet; dvostruki magSensorMax = 0; private void alignMotor () {
// Pronađite magnet
top. DataInterval = top. MinDataInterval;
sawMagnet = false;
magSensor. SensorChange += magSensorStopMotor; top. VelocityLimit = -1000;
int tryCount = 0;
probaj opet:
top. TargetPosition += stepsPerRev;
while (top. IsMoving &&! sawMagnet) Thread. Sleep (25);
if (! sawMagnet) {
if (tryCount> 3) {Console. WriteLine ("Poravnanje nije uspjelo"); top. Angaged = false; bottom. Engaged = false; runtest = false; povratak; }
tryCount ++;
Console. WriteLine ("Jesmo li zapeli? Pokušavamo izraditi sigurnosnu kopiju …"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep (100);
goto tryagain;
}
vrh. Ograničenje brzine = -100;
magData = novi popis> (); magSensor. SensorChange += magSensorCollectPositionData; top. TargetPosition += 300; while (top. IsMoving) Thread. Sleep (100);
magSensor. SensorChange -= magSensorCollectPositionData;
top. VelocityLimit = -topVelocityLimit;
KeyValuePair max = magData [0];
foreach (KeyValuePair par u magData) if (pair. Value> max. Value) max = pair;
top. AddPositionOffset (-max. Key);
magSensorMax = max. Vrijednost;
top. TargetPosition = 0;
while (top. IsMoving) Thread. Sleep (100);
Console. WriteLine ("Poravnanje uspjelo");
}
Popis> magData;
private void magSensorCollectPositionData (pošiljatelj objekta, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {magData. Add (new KeyValuePair (top. Position, e. SensorValue)); }
private void magSensorStopMotor (pošiljatelj objekta, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {
if (top. IsMoving && e. SensorValue> 5) {top. TargetPosition = top. Position - 300; magSensor. SensorChange -= magSensorStopMotor; sawMagnet = istina; }}
Konačno, donjim motorom upravlja se slanjem u jedan od položaja spremnika kuglica. Za ovaj projekt imamo 19 pozicija. Algoritam bira najkraći put i okreće se u smjeru kazaljke na satu ili u suprotnom smjeru.
private int BottomPosition {get {int posn = (int) bottom. Position % stepsPerRev; if (posn <0) posn += stepsPerRev;
return (int) Math. Round ((((posn * beadCompartments) / (double) stepsPerRev));
} }
private void SetBottomPosition (int posn, bool wait = false) {
posn = posn % kuglicaOdjeljenja; double targetPosn = (posn * stepsPerRev) / beadCompartments;
dvostruka strujaPosn = dno. Položaj % stepsPerRev;
dvostruki posnDiff = targetPosn - currentPosn;
// Zadržite to kao potpune korake
posnDiff = ((int) (posnDiff / 16)) * 16;
if (posnDiff <= 1600) bottom. TargetPosition += posnDiff; else bottom. TargetPosition - = (stepsPerRev - posnDiff);
ako (čekaj)
while (dolje. Pokreće se) Navoj. Spavanje (50); }
Fotoaparat
OpenCV se koristi za čitanje slika s web kamere. Nit kamere pokreće se prije pokretanja glavne niti za sortiranje. Ova nit kontinuirano čita slike, izračunava prosječnu boju za određenu regiju koristeći Mean i ažurira globalnu varijablu boja. Nit također koristi HoughCircles kako bi pokušao otkriti ili perlicu, ili rupu na gornjoj ploči, kako bi poboljšao područje koje gleda radi otkrivanja boje. Prag i HoughCircles brojevi utvrđeni su pokušajem i pogreškom i uvelike ovise o web kameri, osvjetljenju i razmaku.
bool runVideo = true; bool videoRunning = false; VideoCapture snimanje; Tema cvThread; Boja otkrivenaBoja; Logičko otkrivanje = lažno; int detectionCnt = 0;
private void cvThreadFunction () {
videoRunning = false;
snimanje = novi VideoCapture (odabrana kamera);
pomoću (Window window = new Window ("capture")) {
Mat slika = nova Mat (); Mat slika2 = novi Mat (); while (runVideo) {hvatanje. Read (slika); if (image. Empty ()) break;
ako (otkrivanje)
detektirajCnt ++; else detektirajCnt = 0;
if (otkrivanje || circleDetectChecked || showDetectionImgChecked) {
Cv2. CvtColor (image, image2, ColorConversionCodes. BGR2GRAY); Mat ham = image2. Threshold ((double) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); ham = ham. GaussianBlur (novi OpenCvSharp. Size (9, 9), 10);
if (showDetectionImgChecked)
slika = mlata;
if (otkrivanje || circleDetectChecked) {
CircleSegment perlica = mlatara. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 20, 200, 100, 20, 65); if (bead. Length> = 1) {image. Circle (perle [0]. Center, 3, new Scalar (0, 100, 0), -1); image. Circle (perle [0]. Center, (int) perle [0]. Radius, novi Skalar (0, 0, 255), 3); if (kuglica [0]. Radius> = 55) {Properties. Settings. Default.x = (decimalna) perla [0]. Center. X + (decimalna) (kuglica [0]. Radius / 2); Properties. Settings. Default.y = (decimalna) kuglica [0]. Center. Y - (decimalna) (kuglica [0]. Radius / 2); } else {Properties. Settings. Default.x = (decimalna) kuglica [0]. Center. X + (decimalna) (kuglica [0]. Radius); Properties. Settings. Default.y = (decimalna) kuglica [0]. Center. Y - (decimalna) (kuglica [0]. Radius); } Svojstva. Settings. Default.size = 15; Svojstva. Settings. Default.height = 15; } else {
CircleSegment krugovi = ham. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 5, 200, 100, 60, 180);
if (krugovi. Dužina> 1) {Popis xs = krugovi. Odaberite (c => c. Center. X). ToList (); xs. Sort (); Popis ys = krugovi. Odaberite (c => c. Center. Y). ToList (); ys. Sort ();
int medijaanX = (int) xs [xs. Count / 2];
int medijanY = (int) ys [ys. Count / 2];
if (medianX> slika. Širina - 15)
medianX = slika. Širina - 15; if (medianY> image. Height - 15) mediaanY = image. Height - 15;
image. Circle (medijanX, medijanY, 100, novi skalar (0, 0, 150), 3);
if (otkrivanje) {
Svojstva. Settings. Default.x = medianX - 7; Properties. Settings. Default.y = medijanY - 7; Properties. Settings. Default.size = 15; Svojstva. Settings. Default.height = 15; }}}}}
Rect r = nova svojstva Rect ((int). Postavke. Default.x, (int) Properties. Settings. Default.y, (int) Properties. Settings. Default.size, (int) Properties. Settings. Default.height);
Mat beadSample = new Mat (slika, r);
Skalarna avgColor = Cv2. Mean (uzorak kuglica); selectedColor = Color. FromArgb ((int) avgColor [2], (int) avgColor [1], (int) avgColor [0]);
image. Rectangle (r, novi skalar (0, 150, 0));
window. ShowImage (slika);
Cv2. WaitKey (1); videoRunning = istina; }
videoRunning = false;
} }
private void cameraStartBtn_Click (pošiljatelj objekta, EventArgs e) {
if (cameraStartBtn. Text == "start") {
cvThread = nova nit (novi ThreadStart (cvThreadFunction)); runVideo = istina; cvThread. Start (); cameraStartBtn. Text = "stop"; while (! videoRunning) Thread. Sleep (100);
updateColorTimer. Start ();
} else {
runVideo = false; cvThread. Join (); cameraStartBtn. Text = "start"; }}
Boja
Sada možemo odrediti boju zrna i na temelju te boje odlučiti u koji spremnik ga staviti.
Ovaj se korak oslanja na usporedbu boja. Želimo moći razlikovati boje kako bismo ograničili lažno pozitivne rezultate, ali i dopustiti dovoljan prag za ograničavanje lažno negativnih. Usporedba boja zapravo je iznenađujuće složena jer način na koji računala pohranjuju boje kao RGB i način na koji ljudi percipiraju boje nemaju linearnu korelaciju. Da stvar bude gora, potrebno je uzeti u obzir i boju svjetla pod kojom se boja promatra.
Postoje složeni algoritmi za izračunavanje razlike u boji. Koristimo CIE2000, koji daje broj blizu 1 ako se 2 boje ne bi mogle razlikovati od čovjeka. Za složene izračune koristimo biblioteku ColorMine C#. Utvrđeno je da vrijednost DeltaE od 5 nudi dobar kompromis između lažno pozitivnog i lažno negativnog.
Budući da često postoji više boja nego spremnika, posljednja pozicija rezervirana je kao spremna kanta. Općenito sam ih ostavio po strani da mogu proći kroz stroj na drugom prolazu.
Popis
colours = new List (); List colorPanels = novi List (); Popis bojaTxts = novi List (); List colorCnts = novi List ();
const int numColorSpots = 18;
const int unknownColorIndex = 18; int findColorPosition (Boja c) {
Console. WriteLine ("Traženje boje …");
var cRGB = novi Rgb ();
cRGB. R = c. R; cRGB. G = c. G; cRGB. B = c. B;
int bestMatch = -1;
dvostruko podudaranjeDelta = 100;
for (int i = 0; i <boje. Broj; i ++) {
var RGB = novi Rgb ();
RGB. R = boje . R; RGB. G = boje . G; RGB. B = boje . B;
double delta = cRGB. Compare (RGB, novi CieDe2000Comparison ());
// dvostruka delta = deltaE (c, boje ); Console. WriteLine ("DeltaE (" + i. ToString () + "):" + delta. ToString ()); if (delta <matchDelta) {matchDelta = delta; bestMatch = i; }}
if (matchDelta <5) {Console. WriteLine ("Pronađeno! (Posn:" + bestMatch + "Delta:" + matchDelta + ")"); vratiti bestMatch; }
if (colors. Count <numColorSpots) {Console. WriteLine ("Nova boja!"); boje. Dodaj (c); this. BeginInvoke (nova radnja (setBackColor), novi objekt {boje. Broj - 1}); writeOutColors (); return (boje. Broj - 1); } else {Console. WriteLine ("Nepoznata boja!"); return unknownColorIndex; }}
Logika razvrstavanja
Funkcija razvrstavanja okuplja sve dijelove kako bi zapravo razvrstali perlice. Ova funkcija radi u namjenskoj niti; pomicanje gornje ploče, otkrivanje boje perli, stavljanje u kantu, pobrinite se da gornja ploča ostane poravnata, brojanje perlica itd. Također prestaje s radom kada se spremnik za hvatanje napuni - U protivnom ćemo jednostavno završiti s prelijevanjem perlica.
Boyean runtest = false; void colourTest () {
ako (! vrh. Zaručen)
top. Angaged = true;
if (! dno. Angažirano)
bottom. Engaged = true;
while (runtest) {
nextMagnet (istina);
Thread. Sleep (100); pokušajte {if (magSensor. SensorValue <(magSensorMax - 4)) alignMotor (); } uhvati {alignMotor (); }
nextCamera (istina);
otkrivanje = istina;
while (detectionCnt <5) Thread. Sleep (25); Console. WriteLine ("Broj otkrivanja:" + detektirajCnt); otkrivanje = lažno;
Boja c = otkrivenaBoja;
this. BeginInvoke (nova radnja (setColorDet), novi objekt {c}); int i = findColorPosition (c);
SetBottomPosition (i, istina);
nextHole (istina); colorCnts ++; this. BeginInvoke (nova radnja (setColorTxt), novi objekt {i}); Thread. Sleep (250);
if (colorCnts [unknownColorIndex]> 500) {
top. Angaged = false; bottom. Engaged = false; runtest = false; this. BeginInvoke (nova radnja (setGoGreen), null); povratak; }}}
private void colourTestBtn_Click (pošiljatelj objekta, EventArgs e) {
if (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = nova nit (novi ThreadStart (colourTest)); runtest = true; colourTestThread. Start (); colourTestBtn. Text = "STOP"; colourTestBtn. BackColor = Boja. Crvena; } else {runtest = false; colourTestBtn. Text = "IDI"; colourTestBtn. BackColor = Boja. Zelena; }}
U ovom trenutku imamo radni program. Neki dijelovi koda izostavljeni su iz članka, pa pogledajte izvor da biste ga zapravo pokrenuli.
Druga nagrada na natjecanju iz optike
Preporučeni:
Robot za sortiranje recikliranja: 15 koraka (sa slikama)
Robot za sortiranje recikliranja: Jeste li znali da se prosječna stopa zagađenja u zajednicama i tvrtkama kreće do 25%? To znači da se svaki četvrti komad recikliranja koji bacite ne reciklira. To je uzrokovano ljudskom pogreškom u centrima za recikliranje. Traditi
Robotsko kućište "uradi sam": 8 koraka (sa slikama)
Diy Robot Chassis: ovo je najjednostavnije robotsko kućište koje možete napraviti kod kuće. Izradu videa možete pogledati na mom KANALU.Možete se izravno PRETPLATITI NA MOJ KANAL KLIKNITE OVDJE
KAKO NAPRAVITI NEVEROVATNO ROBOTSKO KRAVINO OŽALJIVANJE: 16 koraka (sa slikama)
KAKO NAPRAVITI ZADIVLJIVO ŠTATILO OD KRAVE ROBOTA: Nedavno sam stvorio Moo-Bota, strašilo robota krava koje skače preko Mjeseca, za lokalno natjecanje strašila. Inspiracija mi je bio moj sin koji je pjevao " hey dodle dodle, mačka i gusle. .. " Na projektu je bilo jako zabavno raditi s mojim
Mašina za sortiranje čarobnog mramora LittleBits: 11 koraka (sa slikama)
LittleBits Magični stroj za sortiranje mramora: Jeste li ikada htjeli sortirati mramore? Tada biste mogli napraviti ovaj stroj. Više nikada nećete morati prelistavati vreću mramora! To je čarobni stroj za sortiranje mramora, koji koristi senzor boje za Adafruit, tip TCS34725 i Leonarda Arduina iz
Stroj za sortiranje vijaka: 7 koraka (sa slikama)
Stroj za sortiranje vijaka: Jednog dana u laboratoriju (FabLab Moskva) vidio sam svog kolegu zauzetog sortiranjem pune kutije vijaka, matica, prstenova i drugog hardvera. Zastavši kraj njega, sekundu sam gledao i rekao: " Bio bi to savršen posao za stroj. &Quot; Nakon kratkog pogleda