Përmbajtje:

Projekti Guitar Hero Arduino: 12 hapa (me fotografi)
Projekti Guitar Hero Arduino: 12 hapa (me fotografi)

Video: Projekti Guitar Hero Arduino: 12 hapa (me fotografi)

Video: Projekti Guitar Hero Arduino: 12 hapa (me fotografi)
Video: Робот-бот на Arduino своими руками. Для игры Guitar Hero. (PCBWay) 2024, Nëntor
Anonim
Projekti Guitar Hero Arduino
Projekti Guitar Hero Arduino
Projekti Guitar Hero Arduino
Projekti Guitar Hero Arduino
Projekti Guitar Hero Arduino
Projekti Guitar Hero Arduino

Wij zijn Maarten Vrebos, Justin Cavanas en Wannes Stroobandt dhe ne studiojmë multimedia dhe teknologji komunikimi. Ju mund të përdorni projekte të tjera Audiovisual & IT Principles që do të përdorin Guitar Hero-gitaar dhe do të përdorin një kontrollues MIDI. Het is onze bedoeling om de bestaande knoppen op de gitaar intern te vervangen. Kontrolluesi i kontrollit zal vastgehouden en bespeeld worden als een normale gitaar. Aangezien we iets hebben gehackt hebben we er niet veel extra materiaal in moeten verwerken.

In de afbeelding kan u onze allereerste schets op papier zien van hoe het eindproduct er zou moeten uitzien met daarnaast een foto van gitaar die als behuizing zal worden gebruikt.

Wij hebben ons voor dit project gebaseerd op volgende bronnen:

slapyak.wordpress.com/guitar-hero-midi-con…

www.instructables.com/id/Converting-a-rescu…

gizmodo.com/391834/turn-your-guitar-hero-g…

Benodigdheden voor dit projekt

  • 6 butona kleine
  • 7 Rezistorë 1kohm
  • 1 LED LED 1
  • blauwe LED
  • 1 Arduino Uno R3
  • 1 LED i zakonshëm
  • 2 LED të hipur
  • 1 schuifschakelaar
  • 1 dërrasë buke
  • 1 potenciometër
  • 1 protobord
  • 1 kitarë Hero gitaar
  • Voldoende në shtrat
  • Materiaal om te solderen/dremelen/
  • Schroevendraaier

Hapi 1: Komponenti Verzamelen

Komponenti Verzamelen
Komponenti Verzamelen

Prototipi i Voor ons (op breadboard) hebben we volgende componenten gebruikt:

6 butona shtytës

7 Rezistorët 1kohm

1 LED i verdhë

1 LED blu

1 Arduino Uno R3

1 LED i gjelbër

2 LED të kuq

1 Schuifschakelaar

1 Dërrasë buke

1 Potenciometër

Hapi 2: Prototipi Bouwen

Prototipi Bouwen
Prototipi Bouwen
Prototipi Bouwen
Prototipi Bouwen
Prototipi Bouwen
Prototipi Bouwen

Om ons prototype te bouwen hebben we al onze componenten gebruikt op een breadboard, the breadboard dient dan als testobject zodat ne niet meteen in de behuizing te werk moeten gaan. Dot prototipi hebben we dan ook gedigitaliseerd nëpërmjet tinkercad.com, në mënyrë më të detajuar që ne kishim një prototip të tepërt të prototipit të të dhënave të tjera.

Me fjalë të tjera 5 butona shtytës mund të përdoren për të kontrolluar 5 të dhëna dhe të dhëna në kombinim me kombinimin e një pjese të madhe të 'snaren' të fjalës së ingedrukturës ose të efektit të auditorit në krijim. Për shembull, LED-llambat mund të kontrollohen nga pamja e jashtme e të dhënave të ndërveprimit të suksesshëm të tyre.

Hapi 3: Prototipi i kodit

Prototipi i kodit
Prototipi i kodit

Globale variabelen

Në het eerste deel van de code inicializues je globale variabelen voor de pin tuaj në arduino uno waar alle pushbuttons mee verbonden zijn.

// numrat e pin -it mund të gjenden në butonin mainButton (kliko) dhe klikoni në butonat e mëparshëm të zijn: const int mainButton = A1; // gitaar snaar const int lightSensor = A0; const int buttonPin1 = 2; // nummer van pushbutton1 const int buttonPin2 = 3; // nummer van pushbutton2const int buttonPin3 = 4; // nummer van pushbutton3const int buttonPin4 = 5; // nummer van pushbutton4const int buttonPin5 = 6; // butoni i numerit 5

Fjalëkalimi mund të thuhet për vargjet që mund të përdoren për t'i përdorur butonat e shtytjes në numrin e fundit.

const int aantalKnoppen = 5; const String namenKnoppen [aantalKnoppen] = {"knop 1", "knop 2", "knop 3", "knop 4", "knop 5"}; const int knopPinnen [aantalKnoppen] = {2, 3, 4, 5, 6};

Për më tepër, ju mund t'i përdorni ato në LED lichtjes.

const int ledPin1 = 13; // numri i kunjit LED 13

const int ledPin2 = 12; // numri i kunjit LED 12 const int ledPin3 = 11; // numri i kunjit LED 11 const int ledPin4 = 10; // numri i kunjit LED 10 const int ledPin5 = 9; // numri i kunjit LED 9 const int potPin = A5; // numri i kunjit LED A5

De laatste globale variabelen dienen als 'voor de sensorë (mund të përdorni butona të ndryshëm të potenciometrit, lichtsensorit).

// button initialiseerStates voor de knoppen (ingedrukt of niet) int mainButtonState = 0; butoni int Shteti1 = 0; butoni int Shteti2 = 0; butoni int Shteti3 = 0; butoni int Shteti4 = 0; butoni int Shteti5 = 0; int lightSensorState = 0; int potVlera = 0; int lightValue = 0;

Konfigurimi

Nuk mund të zbardhë funksionimin e konfigurimit. Deze is van het type void (gjeeft geen waarde terug) dhe udhëzime nga hierin worden maar 1 keer uitgevoerd.

Bij elke functie është një koment që do të thotë se është konkrete në fjalë. Uitleg shtesë mbi atë që specifikoni funksionin konkret do të tregohet në referencën de arduino

void setup () {// shpejtësia e të dhënave për sekondë (baud) ose transmetimi i të dhënave në seri Serial.begin (9600); // Initialiseer de ledPin variabelen als output pinMode (ledPin1, OUTPUT); pinMode (ledPin2, OUTPUT); pinMode (ledPin3, OUTPUT); pinMode (ledPin4, OUTPUT); pinMode (ledPin5, OUTPUT); // inicializuesi i butonave të tjera të dhëna: pinMode (mainButton, INPUT); pinMode (butoniPin1, INPUT); pinMode (butoniPin2, HYRJE); pinMode (butoniPin3, HYRJE); pinMode (buttonPin4, INPUT); pinMode (butoniPin5, INPUT); pinMode (potPin, INPUT); pinMode (ndriçuesi i ndriçimit, HYRJA); }

Funksioni i zbrazët

Në rregullimin () funksionin volgt de loop () funksionin, udhëzimet vdiqën në këtë mënyrë për të folur.

lak i pavlefshëm () {// shikoni të dhënat nga butonat e shtytjes (ingedrukt of niet) mainButtonState = digitalRead (mainButton); buttonState1 = digitalRead (butoniPin1); buttonState2 = digitalRead (butoniPin2); buttonState3 = digitalRead (butoniPin3); buttonState4 = digitalRead (butoniPin4); buttonState5 = digitalRead (butoniPin5);

// alle statusbutton statusen në een array

int buttonStates = {buttonState1, buttonState2, buttonState3, buttonState4, buttonState5};

// për të mos u përdorur nga potenciometri dhe licensensori

potValue = analogRead (potPin); lightValue = analogRead (lightSensor);

// deklaro een array mainStates en geef die nga standarde waarden 0 in.

int mainStates = {0, 0, 0, 0, 0};

// lak mbi de array aantalKnoppen

për (int i = 0; i <aantalKnoppen; i ++) {pinMode (knopPinnen , INPUT); // inicializuesi alle knopPinnen als input digitalRead (knopPinnen ); // lees de waarde van alle knoppinnen uit // indien de mainswitch (snaar) ingedrukt is, print all knopnamen, alle buttonstates if (mainButtonState == HIGH) {Serial.print (namenKnoppen ); Serial.print (","); Serial.println (buttonStates ); }}

Hapi 4: Prototipi Uittesten

Nadat het prototype gebouwd is volgens ons model en de code geschreven është në Processing, is het tijd om het prototype uit te testen. Op de video është te zien dat alle knoppen een reagie geven op de bijhorende ledjes në dat ook kombinime nga knoppen mogelijk zijn.

Në videon e tweede është e mundur të përdorni një potenciometër në dorën tuaj ose në një mënyrë të tillë për të përdorur fjalë në Ushqim në Përpunim.

Hapi 5: Behuizing "ontmantelen" En Kijken Welke Componenten Gebruikt Gaan Worden

Behuizing
Behuizing
Behuizing
Behuizing
Behuizing
Behuizing
Behuizing
Behuizing

Të gjitha kodet e sakta u bënë me prototipin e tij, ne filluam të takoheshim me "ontmantelen" nga on Guitar Hero-gitaar. Ne e kuptuam hapjen e një gitaar të njohur me të gjithë të dhënat e para të origjinës së komponentit që ne nuk mund të kontrollonim një kontrollues të mëparshëm. Përdorni ato për të shtypur butonat e nevojshëm në butonat më të mirë të zgjedhjes (zie volgende stap). Ne dëgjojmë se si të lexoni të dhënat eindroduktit dhe të dhëna të tjera në butonin (filloni butonin e të gjithëve dhe kombinoni atë të parë) hebben we ook de originele twee buttons gebruikt (zie vierde foto). De LEDjes zullen verdwijnen (deze waren enkel ter indicatie zodat ne zagen dat alle knoppen correct werkten.

Hapi 6: Duke punuar butonat Originele + Dremelen

Butona duke punuar Originele + Dremelen
Butona duke punuar Originele + Dremelen

Op video e përzgjedhur është duke klikuar në origjinalin tuaj të parë, duke përdorur të gjitha llojet e të dhënave që do të përdorni nga kombinimi i tyre i parë.

Om onze eigen buttons te verwerken in de originele knoppen hebben we de binnenkant van de originelen er grotendeels uitgehaald zoals te zien is op de foto.

Hapi 7: Bedrading Solderen + Buttons Vastlijmen

Bedrading Solderen + Buttons Vastlijmen
Bedrading Solderen + Buttons Vastlijmen
Bedrading Solderen + Buttons Vastlijmen
Bedrading Solderen + Buttons Vastlijmen
Bedrading Solderen + Buttons Vastlijmen
Bedrading Solderen + Buttons Vastlijmen

Omdat ne nuk mund të takohemi me ushqimin e ngushtë, pasi u përdorën në draden gesoldeerd worden om zo de verschillende componenten met elkaar te verbinden. Nevojitet të kuptohet se si të hapni butonat në zoals te zien is op de foto's. Eens dit gebeurd është kunnen we doorgaan naar de volgende stap.

Hapi 8: Plaats Maken in De Behuizing

Plaats Maken në De Behuizing
Plaats Maken në De Behuizing
Plaats Maken në De Behuizing
Plaats Maken në De Behuizing
Plaats Maken në De Behuizing
Plaats Maken në De Behuizing

Omdat dit Guitar Hero-model redelijk krap was om mee te werken hebben we extra plaats moeten maken d.m.v. dremelen. Për më tepër, ne do të përdorim këto mundësi për të mësuar më shumë se çka do të thotë që ne do të shohim se si të vendosim në shtrat në gitaar. Omdat er overal in de binnenkant obstakels waren, waaronder veel buisjes om de vijzen in te bevestigen, hebben we die ook verwijderd om optimaal van de gegeven ruimte gebruik te kunnen maken. Op de vierde en vijfde foto is te zien dat ne in de achterkant van de gitaar een doorgang hebben gecreëerd voor de draden die naar de buttons in gaan omdat de gitaar anders niet meer te sluiten was. En op de laatste foto is te zien dat we de draden die rechtstreeks verbonden worden met de Arduino derën dhe gatin në një onderkant van de gitaar de behuizing verlaten.

Hapi 9: Shtrirja në shtrat e Aansluiten Op Protobord

Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord
Bedrading Aansluiten Op Protobord

Om alle componenten met elkaar te verbinden hebben we gebruik gemaakt van een protobord. Dit is een bordje dat eigenlijk op net dezelfde manier werkt als een breadbord, maar dan betrouwbaarder en efficiënter. We hebben de bedrading aan het bordje gesoldeerd zoals te zien is op de derde foto. Dit bord is het centrale punt van waaruit al onze verbindingen vertrekken en samenkomen (zie foto 2).

Hapi 10: Verstevigen

Verstevigen
Verstevigen

Sa i përket prekjes përfundimtare, është e mundur që të humbisni të dhënat tuaja për një stabilitet shtesë. Op deze foto is te zien hoe we het deel dat we er hebben uitgehaald d.m.v. dremelen achteraan de buttons verstevigen met stukjes karton.

Hapi 11: Code Voor Het Communiceren Met Reaper

Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper
Code Voor Het Communiceren Met Reaper

Kodi Deze është hapur në twee delen, por eerste deel është në de arduino IDE (mjedisi i zhvillimit ndërveprues) geschreven. Die code wordt ngarkoni me arduino zelf në dient om all waarden van sensorë të këtyre kontrollorëve midi dhe ju shpejtoni në derën tuaj të përpunimit të naar.

Përpunimi është het tweede gedeelte. Deze code dient om alles wat arduino doortuurt te ontvangen en door te sturen naar Reaper.

Arduino

/* Ky kod është një skicë bazë për të komunikuar me Përpunimin përmes Serialit.

Shtë një plan në të cilin mund të vendosni kodin tuaj

specifikuar për butonat, potenciometrat ose sensorët tuaj.

Ka një shtrëngim duarsh për t’u siguruar që kemi kontakte

dhe formati në të cilin po komunikojmë është vendosur

Itshtë e rëndësishme që mesazhi të ndërtohet në të njëjtën mënyrë, në mënyrë që Processing të dijë si ta zbërthejë atë dhe të dërgojë mesazhe të sakta OSC tek DAW-ja jonë

bërë për werkcollege AV&IT

tetor 2017

*

/ norma e baudit

konst gjatë baudRate = 115200;

// koha për të pritur në ms midis sondazheve në kunjat

const int loopPauseTime = 200; // milili sekonda

// vlerat e fillimit dhe mbarimit për mesazhin e dërguar në Serial

const String startString = "*", endString = "#";

const char contactCharacter = '|';

// identifikuesit e pin

// ndryshore të tjera globale

const int aantalKnoppen = 5; const String namenKnoppen [aantalKnoppen] = {"knop 1", "knop 2", "knop 3", "knop 4", "knop 5"}; const int knopPinnen [aantalKnoppen] = {2, 3, 4, 5, 6}; const int mainButton = A1;

int mainButtonState = 0;

int potVlera = 0;

// sensorë analoge

const int potPin = A5; // pin voor tremolo

// Ne kemi nevojë për këtë funksion për të vendosur kontakt me skicën e Përpunimit

// Mbajeni këtu të pavlefshme estabContact () {while (Serial.available () <= 0) {Serial.print (contactCharacter); // dërgoni një shenjë dhe prisni një përgjigje … vonesë (loopPauseTime); } Serial.read (); }

void setup () {

// vendosni pinModes për të gjitha kunjat për (int i = 0; i <aantalKnoppen; i ++) {pinMode (knopPinnen , INPUT); } pinMode (mainButton, INPUT); // mos komentoni nëse përdorni sensorë që punojnë në 3V në vend të 5V // do të duhet të lidhni pinin 'ext' në 3.3V gjithashtu // analogReference (EXTERNAL);

// inicializoni Serial comms

Serial.filloni (baudRate); ndërsa (! Serial); // prisni për shtrëngimin e dorës estabContact (); }

lak void () {

// HAPI 1: LEXONI BUTONA // sondoni të gjitha kunjat dhe hartoni leximin në intervalin e duhur të butonit intStates [aantalKnoppen]; /* buttonStates [0] = digitalRead (knopPinnen [0]); buttonStates [1] = digitalRead (knopPinnen [1]); buttonStates [2] = digitalRead (knopPinnen [2]); buttonStates [3] = digitalRead (knopPinnen [3]); buttonStates [4] = digitalRead (knopPinnen [4]); */ mainButtonState = digitalRead (mainButton); për (int i = 0; i <aantalKnoppen; i ++) {buttonStates = digitalRead (knopPinnen ); } potValue = analogRead (potPin); // shembuj: // float v0 = hartë (bpm, 0, 1023, 60, 250); // nëse doni të përdorni një noton të normalizuar (p.sh. për volumin) // float v1 = map (analogRead (pin2), fromMin, fromMax, 0, 100) / 100.0;

// HAPI 2: SHKRONI MESAZH

Serial.print (startString); // filloni një sekuencë mesazhi për (int i = 0; i <aantalKnoppen; i ++) {if (mainButtonState == HIGH) {Serial.print (namenKnoppen ); Serial.print (","); Serial.print (buttonStates ); nëse (i <aantalKnoppen - 1) {Serial.print (","); }} else {buttonStates = 0; Serial.print (namenKnoppen ); Serial.print (","); Serial.print (buttonStates ); nëse (i <aantalKnoppen - 1) {Serial.print (","); }}} Serial.print (","); Serial.print ("tremolo"); Serial.print (","); Serial.print (harta (potValue, 0, 1023, 0, 100)); // shkruaj fundin e mesazhit Serial.print (endString);

// prit pak..

vonesë (loopPauseTime); }

Përpunimi

Disclaimer: Niet alle code van përpunimin e skicës staat hier në geschreven, ose në kodin volledige zie het bestand: ProcessingSoundControl_handout_v6_1.pde në bijlazh

Udhëzimet udhëzuese për një fjalë të fundit (indien nodig):

// Baudrate moet hetzelfde zijn zoals në skicë de arduino

int int baudRate = 115200;

// Adresa IP e Zoek naar het në korrëse (pamjet e ekranit në bijlazh)

// Përpunimi i stuurt naar dit andres en reaper luistert hier naar //

// tela e largët String IP = "192.168.1.43"; // p.sh. "127.0.0.1";

telekomandë përfundimtare IP = "10.3.209.60";

// Merrni shënim sendPort dhe plotësojeni këtë në Reaper.

// Ky është porti që përpunimi dërgon dhe dëgjon Reaper.

porta përfundimtare e dëgjimit = 12000, sendPort = 12000;

// Porta e dëgjimit këtu është të korrigjosh në mënyrë aktive.

// emrat e portit janë këtu për të korrigjuar gjithashtu.

// emri i fundit String portName = "/dev/ttyACM0";

emri i portës përfundimtare të vargut = "COM5"; // "/dev/ttyUSB0";

////////////////////// FUNDI I PARAMETRAVE TER PERRDORUESIT //////////////////////// ////

përpunimi i importit.serial.*;

import java.util.*;

importo oscP5.*;

importi neto5.*;

OscP5 oscP5;

NetAddress myRemoteLocation;

Serial commsPort; // Porti serik

boolean messageArrived = false;

Vargu në hyrje = "", IncomingOSCMessage = "";

fillimi i karbonit përfundimtarChar = '*', endChar = '#'; contact char përfundimtarCharacter = '|';

// Për tu siguruar që ne dërgojmë vetëm parametrat (vlerat) që ndryshojnë

// këto variabla globale shfaqen këtu, por nuk duhet të inicializohen këtu! HashMap oldParams, newParams, toSendParams;

// Ne duhet ta ndajmë mesazhin në çdo presje

void processIncoming () {String resVec = incoming.split (","); // marrim çifte emri+vlerash // kështu që për çdo emër (+2)… provoni {për (int i = 0; i <resVec.length; i+= 2) {vlera e notit = Float.parseFloat (resVec [i+ 1]); // vendosini ato në HasPtable newParams.put (resVec , vlera); }} // nëse ndodh një gabim, le ta kapim atë të shfaqet dhe të dalë. kap (Përjashtim ex) {println ("Mesazhi i Përjashtimit:" + ish); printArray (resVec); dalje (); }}

// Për të filtruar mesazhet tona

/ * Ne sigurohemi që ka vetëm një mesazh OSC kur * mesazhi hyrës (Serial) ndryshon * Kjo është: nëse e kthejmë/shtypim butonin dhe ai ndryshon vlerën. * Kështu që ne filtrojmë vlerat hyrëse që ndryshojnë në të vërtetë HashMap (); për (Çelësi i vargut: newParams.keySet ()) {// nëse çelësi është tashmë i pranishëm nëse (oldParams.containsKey (çelësi)) {// çelësi i pranishëm dhe vlera nuk është e njëjtë, atëherë përditësoni nëse (! oldParams.get (çelësi). barazohet (newParams.get (çelësi))) {toSendParams.put (çelësi, newParams.get (çelësi)); }} else {// çelësi nuk është i pranishëm në parametrat e vjetër, prandaj vendoseni! toSendParams.put (çelësi, newParams.get (çelësi)); } oldParams.put (çelësi, newParams.get (çelësi)); }}

void makeOSC () {

për (Çelësi i vargut: toSendParams.keySet ()) {OscMessage myMessage = OscMessage i ri ("/"+ çelës); myMessage.add (toSendParams.get (kyç)); / * dërgoni mesazhin */ oscP5.send (myMessage, myRemoteLocation); }}

void translateMessage () {

processIncoming (); filterParams (); makeOSC (); } // Kur duam të printojmë në dritare void ShowIncoming () {// për të parë mesazhin në hyrje, siç është vendosur në tekstin HashMap ("Hyrja nga Arduino", 20, 20); int y = 20; për (Çelësi i vargut: newParams.keySet ()) {y = y+20; tekst (çelësi, 20, y); tekst (newParams.get (kyç), 300, y); }}

void showOsc () {

tekst (IncomingOSCMessage, 300, 200); IncomingOSCMessage = ""; }

void setup () {

madhësia (1000, 800); // Plotësoni madhësinë e fazës (255); sfond (0); oldParams = e re HashMap (); newParams = e re HashMap (); // printArray (Serial.list ()); commsPort = Serial i ri (ky, emri i portit, baudRate);

/ * filloni oscP5, duke dëgjuar mesazhet hyrëse */

oscP5 = OscP5 i ri (ky, listenPort);

/* myRemoteLocation është një AdAdress. një NetAddress merr 2 parametra, * një adresë IP dhe një numër porti.myRemoteLocation përdoret si parametër në * oscP5.send () kur dërgoni pako osc në një kompjuter, pajisje, * aplikacion tjetër. përdorimi shihni më poshtë. për qëllime testimi porti i dëgjimit * dhe porta e adresës së largët të vendndodhjes janë të njëjta, prandaj ju * do të dërgoni mesazhe përsëri në këtë skicë. */ myRemoteLocation = NetAddress e re (IP e largët, sendPort); }

tërheqje e pavlefshme () {

if (messageArrived) {background (0); translateMessage (); ShowIncoming (); messageArrived = false; } showOsc (); }

void serialEvent (Serial commsPort) {

// lexoni një bajt nga porti serik: char inChar = commsPort.readChar (); switch (inChar) {case contactCharacter: commsPort.write (contactCharacter); // kërkoni më shumë println ("duke filluar …"); pushim; rasti startChar: hyrëse = ""; pushim; rasti endChar: messageArrived = true; // println ("fundi i mesazhit"); pushim; parazgjedhje: hyrëse += nëChar; pushim; }}

/* mesazhi hyrës osc përcillen në metodën oscEvent. */

void oscEvent (OscMessage theOscMessage) {float value = theOscMessage.get (0).floatValue (); // merrni argumentin e parë osc

IncomingOSCMessage += "\ n" +

String.format ("### mori një mesazh osc:" + "addrpattern:" + theOscMessage.addrPattern () + ": %f", vlerë); println (IncomingOSCMessage); }

Hapi 12: Kontrolluesi Uittesten

Nu alles është aangesloten, kodi tjetër është geschreven en alles është gedubbelcheckt is het eindelijk tijd om de controller z'n werk te laten doen. Ju mund të lexoni efektin e Reaper në gjenetin tuaj të volitoide Guitar Hero MIDI Controller!

Recommended: