Viernes, 24 de Octubre de 2025

Controlando un servomotor desde el PC

26 de enero de 2023
m

En el artículo de hoy te cuento cómo controlar un servomotor desde tu ordenador, a tu gusto, y a través de una interfaz gráfica sencilla y atractiva,

¿Cómo? Pues haremos uso de Arduino y de la comunicación serie que te expliqué en este artículo. Y es que encender y apagar LEDs o un display de 7 segmentos está muy bien, pero qué te parece si empezamos a mover cosas.

¿Qué es un servomotor?

Un servomotor, comúnmente llamado servo, se diferencia de un motor en que, además del propio motor, integra en el mismo dispositivo un sensor de posición, de modo que nos permite situar el eje del motor en una posición determinada, en lugar de girar indefinidamente mientras disponga de alimentación eléctrica.

Podemos decir, por tanto, que un servo es un sistema de bucle cerrado con retroalimentación.

Si quieres más detalles al respecto, puedes encontrar numerosísimas referencias en la web. Si ya conoces la teoría, vamos adelante con el proyecto.

Objetivo del proyecto

En la mayoría de tutoriales que encontrará por intenet te enseñarán los very basics de control de un servomotor. En jjosemayorga te voy a mostrar un proyecto más complejo (no mucho más complejo, no estamos construyendo una central nuclear).

En este proyecto vamos a desarrollar una aplicación con Java desde la que podremos enviar en tiempo real la posición que deseamos en nuestro servomotor y, para que sepas desde ya el resultado que vas a obtener, te enseño la demo del proyecto.

Como ves, la aplicación contará con una interfaz gráfica muy atractiva en la que podremos:

  1. Seleccionar el puerto serie y el baudrate que queremos emplear en la comunicación. Esto nos permitirá conectar fácilmente con el dispositivo que queramos desde la propia aplicación sin tener que modificar el código.
  2. Introducir la posición deseada para el servo a través de un slider.
  3. Ver de manera gráfica la posición del servo.

Montaje físico

Para este proyecto necesitamos:

El montaje es muy sencillo, ya que el servo sólo necesita tres conexiones: una para la tensión de alimentación, otra para la tierra GND y otra para la señal de mando.

Código Arduino

Para controlar el servo debemos hacer uso de la librería servo.h, la incluiremos al principio del código.

Con esta librería definiremos un objeto de la clase Servo y podremos hacer uso de susmétodos específicos como attach y write.

String readString;
#include <Servo.h>
Servo myservo;  // creamos un objeto de la clase Servo

void setup() {
  Serial.begin(115200);
  myservo.write(90); // ponemos la posicion inicial en 90
  myservo.attach(9);  // la señal de control sale del pin 9
  Serial.println("servo-test-22-dual-input"); 
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();  // lectura del puerto serio
    readString += c; 
    delay(2); 
  }

  if (readString.length() >0) {
    Serial.println(readString);  
    int n = readString.toInt(); 

    // verificamos si la entrada de señal es valida
    if(n >= 500)
    {
      //Serial.print("writing Microseconds: ");
      Serial.println(n);
      myservo.writeMicroseconds(n);
    }
    else
    {   
      //Serial.print("writing Angle: ");
      Serial.println(n);
      myservo.write(n);
    }

    readString=""; 
  }

}

Código Java

El código de la aplicación Java cumple las premisas marcadas en Objetivo del proyecto. Se han integrado todas las funcionalidades con la interfaz gráfica.

package servomotorBT;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.metal.MetalSliderUI;


import java.io.*;

import gnu.io.CommPortIdentifier; 
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent; 
import gnu.io.SerialPortEventListener; 
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Enumeration;

public class servoBT implements SerialPortEventListener
{
	int int_posicion=90;
	String str_posicion="90";
	JFrame marco=new JFrame("Control de servo");
	JLabel titulo=new JLabel("<html><font size=10 color=blue>Control de servo por puerto serie</font></html>");
	servomotor SV1=new servomotor(int_posicion);
	JLabel lbl_puerto=new JLabel("Puerto de conexión:");
	String[] str_puerto={"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9","COM10","COM11","COM12","COM13","COM14","COM15","COM16","COM17","COM18","COM19","COM20","COM21","COM22","COM23","COM24","COM25","COM26"};
	JComboBox cmb_puerto=new JComboBox(str_puerto);
	JLabel lbl_baudrate=new JLabel("Baud Rate:");
	String[] str_baudrate={"4800","9600","19200","38400","57600","115200","234000","460800","921600","1382400"};
	JComboBox cmb_baudrate=new JComboBox(str_baudrate);
	JButton btn_conectar=new JButton("Conectar");
	JButton btn_desconectar=new JButton("Desconectar");
	JLabel lbl_conectar=new JLabel(new ImageIcon("lightOFF.png"));
	JTextField txt_posicion=new JTextField("90");
	JSlider sld_posicion=new JSlider(0,179);
	JTextField txt_noticias=new JTextField();
	
	/** The output stream to the port */
	private OutputStream output = null;
	private BufferedReader input;
	SerialPort serialPort;
	private String PORT_NAME = "COM1";
	/** Milliseconds to block while waiting for port open */
	private static final int TIME_OUT = 2000;
	/** Default bits per second for COM port. */
	private static int DATA_RATE = 9600;

	
	
	servoBT()
	{
		marco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		marco.setIconImage(Toolkit.getDefaultToolkit().createImage("servom_png.png"));
		marco.setResizable(false);
		GridBagLayout gridbag=new GridBagLayout();
		GridBagConstraints gbc=new GridBagConstraints();
		marco.setLayout(gridbag);
		gbc.insets=new Insets(5,20,5,20);
		gbc.gridx=0;
		gbc.gridy=0;
		gbc.gridwidth=6;
		gbc.gridheight=1;
		marco.add(titulo,gbc);
		gbc.gridx=0;
		gbc.gridy=1;
		gbc.gridwidth=2;
		gbc.gridheight=4;
		SV1.setBackground(Color.cyan);
		SV1.setSize(400,400);
		marco.add(SV1,gbc);
		gbc.gridx=2;
		gbc.gridy=1;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		gbc.weighty=0.5;
		gbc.anchor = GridBagConstraints.SOUTH; 
		marco.add(lbl_puerto,gbc);
		gbc.gridx=3;
		gbc.gridy=1;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		gbc.fill=GridBagConstraints.HORIZONTAL;
		cmb_puerto.addItemListener(new PORTlistener());
		marco.add(cmb_puerto,gbc);
		gbc.gridx=2;
		gbc.gridy=2;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		gbc.fill=GridBagConstraints.NONE;
		gbc.weighty=0.5;
		gbc.anchor = GridBagConstraints.NORTH; 
		marco.add(lbl_baudrate,gbc);
		gbc.gridx=3;
		gbc.gridy=2;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		cmb_baudrate.addItemListener(new BRlistener());
		marco.add(cmb_baudrate,gbc);
		gbc.gridx=4;
		gbc.gridy=1;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		gbc.anchor = GridBagConstraints.SOUTH;
		gbc.fill=GridBagConstraints.HORIZONTAL;
		btn_conectar.setName("conectar");
		btn_conectar.addMouseListener(new Conectar());
		marco.add(btn_conectar,gbc);
		gbc.gridx=4;
		gbc.gridy=2;
		gbc.gridwidth=1;
		gbc.gridheight=1;
		gbc.anchor = GridBagConstraints.NORTH;
		gbc.fill=GridBagConstraints.HORIZONTAL;
		btn_desconectar.setName("desconectar");
		btn_desconectar.addMouseListener(new Desconectar());
		marco.add(btn_desconectar,gbc);
		gbc.gridx=5;
		gbc.gridy=1;
		gbc.gridwidth=1;
		gbc.gridheight=2;
		gbc.fill=GridBagConstraints.NONE;
		gbc.anchor = GridBagConstraints.CENTER;
		marco.add(lbl_conectar,gbc);
		gbc.gridx=2;
		gbc.gridy=3;
		gbc.gridwidth=4;
		gbc.gridheight=1;
		gbc.weighty=1.0;
		txt_posicion.setPreferredSize( new Dimension( 200, 50 ) );
		txt_posicion.setHorizontalAlignment(JTextField.CENTER);
		txt_posicion.setEditable(false);
		txt_posicion.setBackground(Color.white);
		Font font1 = new Font("SansSerif", Font.BOLD, 30);
		txt_posicion.setFont(font1);
		marco.add(txt_posicion,gbc);
		gbc.gridx=2;
		gbc.gridy=4;
		gbc.gridwidth=4;
		gbc.gridheight=1;
		sld_posicion.setMajorTickSpacing(20);
		sld_posicion.setMinorTickSpacing(5);
		sld_posicion.setPaintTicks(true);
		sld_posicion.setPaintLabels(true);
		sld_posicion.setValue(90);
		gbc.fill=GridBagConstraints.HORIZONTAL;
		sld_posicion.addChangeListener(new SliderListener());
		sld_posicion.addMouseListener(new SliderMListener());
		marco.add(sld_posicion,gbc);
		gbc.gridx=0;
		gbc.gridy=5;
		gbc.gridwidth=6;
		gbc.gridheight=1;
		gbc.fill=GridBagConstraints.HORIZONTAL;
		txt_noticias.setEditable(false);
		txt_noticias.setBackground(Color.white);
		marco.add(txt_noticias,gbc);
		
		marco.pack();
		marco.setLocationRelativeTo(null);
		marco.setVisible(true);
		
	}
	
	class SliderListener implements ChangeListener
	{

		@Override
		public void stateChanged(ChangeEvent e) 
		{
			int_posicion=sld_posicion.getValue();
			str_posicion=String.valueOf(int_posicion);
			txt_posicion.setText(str_posicion);
			SV1.renovar(int_posicion);
			txt_noticias.setText("Posición enviada: "+str_posicion);
			sendData(str_posicion);
		}
		
	}
	
	class SliderMListener extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			 Point p = e.getPoint();
	            double percent = p.x / ((double) sld_posicion.getWidth());
	            int range = sld_posicion.getMaximum() - sld_posicion.getMinimum();
	            double newVal = range * percent;
	            int result = (int)(sld_posicion.getMinimum() + newVal);
	            sld_posicion.setValue(result);
		}
	}
	
	class BRlistener implements ItemListener
	{

		@Override
		public void itemStateChanged(ItemEvent evt) 
		{
			// TODO Auto-generated method stub
			 JComboBox cb = (JComboBox) evt.getSource();
			 String str_DATA_RATE = (String) cb.getSelectedItem();
			 DATA_RATE=Integer.parseInt(str_DATA_RATE);
			 txt_noticias.setText("BaudRate cambiado a: "+str_DATA_RATE);
		}
		
	}
	
	class PORTlistener implements ItemListener
	{

		@Override
		public void itemStateChanged(ItemEvent evt) 
		{
			// TODO Auto-generated method stub
			 JComboBox cb = (JComboBox) evt.getSource();
			 String str_PORT_NAME = (String) cb.getSelectedItem();
			 PORT_NAME=str_PORT_NAME;
			 txt_noticias.setText("Puerto cambiado a: "+str_PORT_NAME);
		}
		
	}
	
	class Conectar extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			Component aux=e.getComponent();
		}
		public void mouseReleased(MouseEvent e)
		{
			Component aux=e.getComponent();
			{
				if(aux.getName().equals("conectar"))
				{
					initialize();
				}
			}
		}
	}
	
	class Desconectar extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			Component aux=e.getComponent();
		}
		public void mouseReleased(MouseEvent e)
		{
			Component aux=e.getComponent();
			{
				if(aux.getName().equals("desconectar"))
				{
					serialPort.removeEventListener();
					serialPort.close();
					lbl_conectar.setIcon(new ImageIcon("lightOFF.png"));
					txt_noticias.setText("Conexión cerrada");
				}
			}
		}
	}
	
	public void initialize() 
	{
		 
		CommPortIdentifier portId = null;
		Enumeration portEnum = CommPortIdentifier.getPortIdentifiers();
 
		// iterate through, looking for the port
		while (portEnum.hasMoreElements()) 
		{
			CommPortIdentifier currPortId = (CommPortIdentifier)
					portEnum.nextElement();
 
			if (PORT_NAME.equals(currPortId.getName())) 
			{
				portId = currPortId;
				break;
			}
		}
		if (portId == null) 
		{
			System.out.println("Could not find COM port.");
			txt_noticias.setText("Conexión fallida");
			return;
		}

		try 
		{
			// open serial port, and use class name for the appName.
			serialPort = (SerialPort) portId.open(this.getClass().getName(),
					TIME_OUT);

			// set port parameters
			serialPort.setSerialPortParams(DATA_RATE,
					SerialPort.DATABITS_8,
					SerialPort.STOPBITS_1,
					SerialPort.PARITY_NONE);

			// open the streams
			input = new BufferedReader(new InputStreamReader(serialPort.getInputStream()));
			output = serialPort.getOutputStream();

			// add event listeners
			serialPort.addEventListener(this);
			serialPort.notifyOnDataAvailable(true);
			lbl_conectar.setIcon(new ImageIcon("lightON.png"));
			txt_noticias.setText("Conexión establecida");
		} 
		catch (Exception e) 
		{
			System.err.println(e.toString());
		}
	}
	

	public synchronized void close() 
	{
		if (serialPort != null) 
		{
			serialPort.removeEventListener();
			serialPort.close();
		}
	}

	
	

	public synchronized void serialEvent(SerialPortEvent oEvent) 
	{
		if (oEvent.getEventType() == SerialPortEvent.DATA_AVAILABLE) 
		{
			try 
			{

			} 
			catch (Exception e) 
			{
				System.err.println(e.toString());
			}
		}
	}
	
	private void sendData(String data)
	{
		 
		try 
		{
			output.write(data.getBytes());
		} catch (IOException e) {
			showError("Error sending data");
			//System.exit(ERROR);
		}
	}
	private void showError(String errorMessage)
	{
		JOptionPane.showMessageDialog(marco,
				errorMessage,
			    "Error",
			    JOptionPane.ERROR_MESSAGE);
	}
	
	public static void main(String[] args)
	{
		try
	    {
	      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
	    }
	    catch(Exception e)
	    {
	      e.printStackTrace();
	    }
		servoBT app=new servoBT();
	}

}

Para la creación del gráfico dinámico necesitaremos crear una clase adicional que herede de la clase Canvas.

package servomotorBT;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class servomotor extends Canvas
{
	int pos;
	
	public servomotor(int posicion)
	{
		pos=posicion;
	}
	
	void renovar(int posicion)
	{
		pos=posicion;
		repaint();
	}
	
	public void paint(Graphics g)
	{
		g.setColor(Color.black);
		g.fillOval(50, 50, 300, 300);
		Graphics2D g2 = (Graphics2D)g;
		g2.setStroke(new BasicStroke(16));
		g.setColor(Color.white);
		g.drawLine((int) (200-175*Math.cos(Math.toRadians(pos))), (int) (200-175*Math.sin(Math.toRadians(pos))), (int) (400-(200-175*Math.cos(Math.toRadians(pos)))), (int) (400-(200-175*Math.sin(Math.toRadians(pos)))));
	}
}

Recursos necesarios

Para que la aplicación se vea de la manera mostrada en el vídeo es necesario contar con los recursos gráficos adecuados. Ya que a veces resulta difícil optimizar el tamaño y los colores de imágenes, gráficos etc., te dejo por aquí los que he empleado yo, para que lo tengas más fácil.

Con esto ya tienes todas las indicaciones para poner en marcha este proyecto. Recuerda que si tienes alguna duda puedes consultarme por cualquiera de las vías que aparecen en la sección Contacto.

Demo

Para finalizar el artículo, os dejo la demo de la aplicación desarrollada.

Publicaciones relacionadas

Controla una Roomba a tu voluntad

Controla una Roomba a tu voluntad

En este artículo te muestro una característica poco conocida de los robots de limpieza Roomba de iRobot: la interfaz abierta de Roomba (o ROI, Roomba Open Interface, por sus siglas en inglés). Esta interfaz es un puerto de conexión serie que permite a Roomba intercambiar mensajes con otros dispositivos como microcontroladores, ordenadores o smartphones. El protocolo de información es público y gracias a él podemos añadir a Roomba cuantas funcionalidades se nos ocurran.

0 comentarios

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *