seth.yang 发表于 2015-7-1 14:03:24

树莓派WIFI小车java版(三)小车java控制程序

本帖最后由 seth.yang 于 2015-7-1 14:56 编辑

前情提要 Previous on 《树莓派WIFI小车java版》

在上一篇,《树莓派WIFI小车java版(二)硬件连接》中,我们做了小手工,将硬件连接起来,这一期,我们编写代码,让这些硬件动起来,并通过网络将车载的摄像头模块拍摄的视频显示出来。 Let's go。

本篇中所有代码可以在https://github.com/seth-yang/rem ... ster/java/Smart-Car下载,您也可以在linux终端中,使用命令
git clone https://github.com/seth-yang/remote-car.git来获取整个工程。

java线程基础

这部分是给对java线程控制不太熟悉的同学看的,若你对java的线程控制很熟悉的话,请直接跳到下一节。
我们需要一个可暂停的线程,线程外部可控制该线程的状态(运行、暂停、退出)。思路很简单,线程监视一个变量,若设置,则等待,否则继续执行线程逻辑,代码如下:package org.dreamwork.smart.car.server.util;

/**
* 可暂停的线程
* Created by seth.yang on 2015/6/10.
*/
public abstract class PausableThread extends Thread {
protected final Object locker = new Object ();
    protected boolean paused = true;
    protected boolean running = false;

      /**
         * 线程逻辑,由具体实现类来决定
         */
    protected abstract void doWork ();

      /**
         * 构造函数,创建一个指定状态的可暂停线程
         */
    public PausableThread (boolean paused) {
      this.paused = paused;
      start ();
    }

      /**
         * 恢复执行线程
         */
    public void proceed () {
      synchronized (locker) {
            paused = false;
            locker.notifyAll ();
      }
    }

      /**
         * 暂停线程
         */
    public void pause () {
      paused = true;
    }

    public boolean isPaused () {
      return paused;
    }

      /**
         * 退出线程,布尔值block表示是否等待线程退出
         */
    public void shutdown (boolean block) throws InterruptedException {
      running = false;
      if (paused)
            proceed ();
      if (block && (Thread.currentThread () != this))
            this.join ();
    }

    @Override
    public synchronized void start () {
      if (!running) {
            running = true;
            super.start ();
      }
    }

    @Override
    public void run () {
      while (running) {
            while (paused) {
                synchronized (locker) {
                  try {
                        locker.wait ();
                  } catch (InterruptedException e) {
                        e.printStackTrace ();
                  }
                }
            }

            doWork ();
      }
    }
}
关于pi4j的简单介绍Pi4j 提供的树莓派IO脚控制的API,入门还是先对简单的,官网提供了一些入门代码,我们来看个最简单的。// 初始化GPIO环境
GpioController gpio = GpioFactory.getInstance();
// 获取一个指定的GPIO脚
GpioPinDigitalOutput pin =
   gpio.provisionDigitalOutputPin(
          RaspiPin.GPIO_01,   // GPIO 脚序号
          "MyLED",             // GPIO脚名称
          PinState.LOW      // 初始状态
   );
// 控制连接GPIO脚的LED闪烁10次
for (int i = 0; i < 10; i ++) {
pin.high ();
Thread.sleep (1000);
pin.low ();
Thread.sleep (1000);
}很简单,是不是。Pi4j对于GPIO的控制代码就是这样大同小异的。好了,我们假定你已经对pi4j入门了,我们继续准备更加深入的代码。
PWM简介
关于PWM的基础概念知识,请自行google/度娘,有很详细的介绍。这里仅介绍pi4j如何控制PWM。Pi4j 1.0 还包括了PCA9685的驱动。不过,对于我来说,Pi4j 的PCA9685驱动有个bug,在com.pi4j.gpio.extension.pca.PCA9685GpioProvider类的第312行,要加一个 mode != null 的判断。改动后的代码在https://github.com/seth-yang/rem ... 85GpioProvider.java可以下载到。同样,我们看一个PCA9685的入门例子,这是一个呼吸灯的代码。
// 获取I2C 总线对象
I2CBus bus = I2CFactory.getInstance(I2CBus.BUS_1);
// 定义 PWM 频率
BigDecimal frequency = new BigDecimal("50");
// 初始化PCA9685驱动
PCA9685GpioProvider provider = new PCA9685GpioProvider(
      bus,            // I2C 总线对象
      0x40,         // I2C 总线地址 对于PCA9685而言,固定为 0x40
      frequency       // PWM 频率
);
Pin led = PCA9685Pin.ALL;    // PCA9685 PWM 0脚
for (int i = 0; i < 10; i ++) {
for (int j = 0; j < 4096; j += 10) {
    if (j < 4096) {
      // 从第0个tick开始到第j个tick结束
      provider.setPWM (led, 0, j);   
      Thread.sleep (1);
    }
}
for (int j = 4096; j > 0; j -= 10) {
    if (j > 0) {
      provider.setPWM (led, 0, j);
      Thread.sleep (1);
    }
}
}其中,provider.setPWM (led, 0, j)这句不太好理解,这里简单解释一下,详细内容请参阅官网文档PCA9685将每个PWM周期等分为4096个时刻(更确切点,称为tick),该方法的第二个参数为高电平的起始tick,第三个参数为高电平的结束时刻,以这种形式来定义PWM的占空比。例如,我们定义占空比为25%的PWM,那么,该方法的参数为:provider.setPWM (led, 0, 1024)它的PWM波形类似
如果将参数改为provider.setPWM (led, 1024, 2048)这同样是一个25%占空比的PWM,不过它的波形为
好了,我们点到为止,毕竟这不是详细介绍PWM的。根据以上的思路,我们使用java语言为PWM抽象一个类出来,接口如下:public class PWM {
    private float value;
      /**
         *构造函数,指定PWM的引脚和频率
         */
public PWM (int pin, int frequency) {
   ...
}
      /**
         * 设置占空比,将原始int, int方式转化为更直观的百分比形式
         */
public void setValue (float value) {
   this.value = value;
   ... // 将百分比映射成 0~4095之间的值
}
      /**
         * 获取当前的占空比
         */
public float getValue () {
    return value;
}
}
舵机控制
舵机的控制信号周期为20ms的脉宽调制(PWM)信号,其中脉冲宽度从0.5-2.5ms,相对应的舵盘位置为0-180度,呈线性变化。也就是说,给他提供一定的脉宽,它的输出轴就会保持一定对应角度上,无论外界转矩怎么改变,直到给它提供一个另外宽度的脉冲信号,它才会改变输出角度到新的对应位置上,如下图所示不难看出,只要给相对应的脉冲宽度(即占空比),就能精确控制舵机停留的角度,根据这个思路,我们也抽象出一个java 对象来表示舵机 (原型,实际代码比这个复杂)/**
* 基于 PCA9685 扩展板的 舵机对象.
*/
public class Servo extends PWM implements TimeoutListener {
    public static enum Direction {
      INCREMENT, DECREMENT
    }

      /**
         * 最小的有效脉宽
         */
    public static final int SERVO_DURATION_MIN = 500;
      /**
         * 中位的脉宽值
         */
    public static final int SERVO_DURATION_NEUTRAL = 1500;
      /**
         * 最大有效脉宽
         */
    public static final int SERVO_DURATION_MAX = 2500;

      /**
         * 用于记录当前舵机停留的角度
         */
    private int angle = 0;
      /**
         * 舵机允许的最小和最大的角度,可通过构造函数指定
         */
    private int minAngle = -90, maxAngle = 90;

    /**
         * 舵机转动方向
         */
    private Direction dir = Direction.INCREMENT;

      /**
         * 事件处理器
         */
    private List<ServoListener> list = new ArrayList<ServoListener> ();

    public Servo (int pin, final int min, final int max) throws IOException {
      super (pin, 50);
    }

      /**
         * 设定舵机的角度
         */
    public void set (int angle) throws InterruptedException {
      if (angle < minAngle)
            angle = minAngle;
      if (angle > maxAngle)
            angle = maxAngle;
      this.angle = angle;
      double tmp = angle + 90;
      // 将角度计算为脉宽
      double duration = SERVO_DURATION_MIN + 100 * tmp / 9;
      if (duration > SERVO_DURATION_MAX)
             duration = SERVO_DURATION_MAX;
      else if (duration < SERVO_DURATION_MIN)
             duration = SERVO_DURATION_MIN;
      provider.setPwm (PCA9685Pin.ALL , (int) duration);
    }

      /**
         * 获取舵机当前停留的角度
         */
    public int get () {
      return angle;
    }

    public void addListener (ServoListener listener) {
      list.add (listener);
    }

    protected void fireListener (int border) {
      for (ServoListener listener : list) {
            listener.onReachBorder (this, border);
      }
    }
}------ 呃,这里发帖子有字数限制,这部分只能拆成2楼了,见谅

seth.yang 发表于 2015-7-1 15:46:23

-- 接楼上

直流电机控制
直流电机是通过L298N控制板进行控制的。一块L298N控制板可以同时控制2路直流电机,其中,每路直流电机需要3根脚进行控制,一根使用PWM信号控制电机的转速,2路数字信号控制电机的转动状态,请参见下表
了解L298N的控制方式后,控制代码比较简单。package org.dreamwork.smart.car.server.component;
public class Motor {
    private GpioPinDigitalOutput pin0, pin1;
    private PWM pwm;
    private int speed = 3;

    public static final int MAX_SPEED = 5, MIN_SPEED = 0;

    public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
      this.pin0 = pin0;
      this.pin1 = pin1;
      this.pwm = pwm;
    }

    public int getSpeed () {
      return speed;
    }

    public void setSpeed (int speed) {
      if (speed < MIN_SPEED) speed = MIN_SPEED;
      if (speed > MAX_SPEED) speed = MAX_SPEED;
      this.speed = speed;
                // 将速度映射为PWM占空比
      pwm.setValue (.2f * speed);
    }

    public void forward () {
      pin0.high ();
      pin1.low ();
    }

    public void backward () {
      pin0.low ();
      pin1.high ();
    }

    public void stop () {
      pin0.low ();
      pin1.low ();
    }

    public void dispose () {
      pin0.low ();
      pin1.low ();
      pwm.setValue (0);
    }
}
增加转向灯这部分对于小车运动控制来说,不是必须的,但可以增加趣味性,同时更加接近现实情况:当车辆转弯时转向灯同时闪烁,指示车辆转弯的方向。这部分的代码相当简单,就是控制一个LED灯的闪烁,唯一不同的事,采用线程,使得LED的闪烁不影响主线程(小车控制命令接收线程)package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.PausableThread;

/**
* Created by seth.yang on 2015/6/8.
*/
public class BlinkLED extends PausableThread {
    private GpioPinDigitalOutput pin;

    public BlinkLED (int pinIndex) {
      super (true);
      pin = GpioHelper.getDigitalOutputPin (pinIndex);
    }

    public boolean isBlinking () {
      return !paused;
    }

    public void blink () {
      proceed ();
    }

    @Override
    protected void doWork () {
      try {
            pin.high ();
            sleep (300);
            pin.low ();
            sleep (300);
      } catch (Exception ex) {
                        // process exception
      }
    }
}
摄像头这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。
拼装小车控制程序小车动作控制部分介绍完了,可以将这些代码拼装成一台完整的小车了。我们的小车需要:l 两路舵机,用于控制摄像头的转动l 四路直流电机(实际上我只用了两路,记得吗,我将左右两侧的2个电机并联了)l 四路转向灯(实际上是也是两路,同上)l 一路照明LED灯(好吧,上面的介绍中没有,但这不影响整体代码)package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.Config;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.Rotate;

import java.io.*;

public class Car implements ServoListener {
    private static final int
            DIR_FORWARD = 1, DIR_BACKWARD = -1, DIR_STOP = 0,
            DIR_TURN_LEFT = -2, DIR_TURN_RIGHT = 2;

        /**
       * 小车当前运动状态(前进,后退或停止)
       */
    private int dir = DIR_STOP;
        /**
       * 小车上一次的运动状态
       */
    private Integer backup_dir = null;
        /**
       * 小车转动状态(左转,右转,停止)
       */
    private Rotate rotate;
        /**
       * 两路转向灯
       */
    private BlinkLED leftLed, rightLed;
        /**
       * 两路舵机
       */
    private Servo servo0, servo1;
        /**
       * 两路直流电机
       */
    private Motor left_front, right_front;
        /**
       * 摄像头
       */
    private Camera camera;
        /**
       * 配置文件
       */
    private Config config;
        /**
       * 前灯
       */
    private GpioPinDigitalOutput led;

    private GpioController gpio;
    private boolean shutdown = false;

    public Car (Config config) throws IOException, InterruptedException {
      this.config = config;
      setup ();
    }

        /**
       * 初始化
       */
    private void setup () throws IOException, InterruptedException {
                ... // 读取配置文件,初始化各个部件
    }

        /**
       * 使指定的LED灯闪烁
       */
    private void toggleBlinkLed (BlinkLED led) {
                ...
    }

        /**
       * 重置小车的各个状态
       */
    public void reset () throws InterruptedException {
      stop ();
      servo0.reset ();
      servo1.reset ();
      camera.close ();
    }

        /**
       * 销毁小车的各个部件,释放资源
       */
    public void dispose () throws InterruptedException {
                ...
    }

    public void toggleLeftBlink () {
      ...
    }

    public void toggleRightBlink () {
      ...
    }

    public void forward () throws InterruptedException {
                ...
    }

    public void backward () throws InterruptedException {
                ...
    }

    public void stop () throws InterruptedException {
                ...
    }

    public void toggleServoLeft () {
                ...
    }

    public void toggleServoRight () {
                ...
    }

    public void toggleServoUp () {
                ...
    }

    public void toggleServoDown () {
                ...
    }

    public void toggleTurnLeft () throws InterruptedException {
                ...
    }

    public void toggleTurnRight () throws InterruptedException {
                ...
    }

    public void toggleLED () {
      led.toggle ();
    }

    public void toggleCamera () throws IOException {
      if (!camera.isOpened ())
            camera.open ();
      else
            camera.close ();
    }

    public void servoLeft () {
      servo0.increase ();
    }

    public void servoRight () {
      servo0.decrease ();
    }

    public void stopHorizontalRotate () {
      servo0.stopRotate ();
    }

    public void servoUp () {
      servo1.decrease ();
    }

    public void servoDown () {
      servo1.increase ();
    }

    public void stopVerticalRotate () {
      servo1.stopRotate ();
    }

    public boolean isRotateUp () {
      return rotate.isRotateUp ();
    }

    public boolean isRotateLeft () {
      return rotate.isRotateLeft ();
    }

    public boolean isRotateDown () {
      return rotate.isRotateDown ();
    }

    public boolean isRotateRight () {
      return rotate.isRotateRight ();
    }
}代码有点长,抱歉没有详细的注释,不过我想,直接看方法名应该就可以看出来这个方法的作用了吧。值得一提的是控制转向。我们的小车是四驱的,并没有转向轮,那么如何转向呢?答案是差速转向:当左右两侧轮子的转速不同时,小车就不再沿直线运动了。设小车左轮的速度为VL,右轮的转速为VR,那么理论上:n 当VL = VR时,转弯半径为无穷大(直线)n 当VL > VR>0时,小车右转,转弯半径 > 轴距n 当VL > VR=0时,小车右转,转弯半径 = 轴距n 当 VR = -VL时,小车右转,转弯半径 = 0反之左转我们的转弯代码取其中一种(我取的是一侧速率为0的方式,这取决于电机的减速比和轮胎的抓地力,如果电机减速比较小或轮胎抓地力较小,建议取VR = -VL,进行原地转弯)...
// 一侧速率为0的转弯方式
private void turnLeft () throws InterruptedException {
        left_front.setSpeed (5);
        right_front.setSpeed (5);
        if (dir == DIR_BACKWARD) {
                left_forward ();
                right_pause ();
      
        } else {
                left_pause ();
                right_forward ();
        }
        leftLed.blink ();
}

// 原地转弯的方式
private void turnLeft () throws InterruptedException {
        left_forward ();
    right_backward ();
        leftLed.blink ();
}
...以上代码二选一,不能同时出现在程序中,否则编译时将出错
指令我们需要指令,来和小车的动作一一对应,枚举它就行了package org.dreamwork.smart.car.server.io;

public enum Command {
    DISPOSE (-2, false),
    QUIT (-1, false),

    STOP ( 0, false),
    FORWARD (1, false),
    BACKWARD (2, false),
    TURN_LEFT (3, false),
    TURN_RIGHT (4, false),

    TOGGLE_LED (5, false),
    TOGGLE_CAMERA (6, false),
    TOGGLE_LEFT_BLINK (7, false),
    TOGGLE_RIGHT_BLINK (8, false),

    STOP_VERTICAL_SERVO (9, false),
    STOP_HORIZONTAL_SERVO (10, false),

    SPEED (11, false),
    SERVO_UP (12, false),
    SERVO_RIGHT (13, false),
    SERVO_DOWN (14, false),
    SERVO_LEFT (15, false),

    LEFT_FORWARD (101, false),
    LEFT_BACKWARD (102, false),
    LEFT_PAUSE (103, false),
    RIGHT_FORWARD (104, false),
    RIGHT_BACKWARD (105, false),
    RIGHT_PAUSE (106, false),

    RESET (501, false)
    ;

    public final int code;
    public final boolean hasReturn;

    private Command (int code, boolean hasReturn) {
      this.code = code;
      this.hasReturn = hasReturn;
    }
}
-- 字数有到了。。。下面还有一楼,介绍剩下的内容,不要走开哦


seth.yang 发表于 2015-7-1 14:44:40

-- 接楼上的内容

直流电机控制
直流电机是通过L298N控制板进行控制的。一块L298N控制板可以同时控制2路直流电机,其中,每路直流电机需要3根脚进行控制,一根使用PWM信号控制电机的转速,2路数字信号控制电机的转动状态,请参见下表了解L298N的控制方式后,控制代码比较简单。package org.dreamwork.smart.car.server.component;
public class Motor {
    private GpioPinDigitalOutput pin0, pin1;
    private PWM pwm;
    private int speed = 3;

    public static final int MAX_SPEED = 5, MIN_SPEED = 0;

    public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
      this.pin0 = pin0;
      this.pin1 = pin1;
      this.pwm = pwm;
    }

    public int getSpeed () {
      return speed;
    }

    public void setSpeed (int speed) {
      if (speed < MIN_SPEED) speed = MIN_SPEED;
      if (speed > MAX_SPEED) speed = MAX_SPEED;
      this.speed = speed;
                // 将速度映射为PWM占空比
      pwm.setValue (.2f * speed);
    }

    public void forward () {
      pin0.high ();
      pin1.low ();
    }

    public void backward () {
      pin0.low ();
      pin1.high ();
    }

    public void stop () {
      pin0.low ();
      pin1.low ();
    }

    public void dispose () {
      pin0.low ();
      pin1.low ();
      pwm.setValue (0);
    }
}
增加转向灯这部分对于小车运动控制来说,不是必须的,但可以增加趣味性,同时更加接近现实情况:当车辆转弯时转向灯同时闪烁,指示车辆转弯的方向。这部分的代码相当简单,就是控制一个LED灯的闪烁,唯一不同的事,采用线程,使得LED的闪烁不影响主线程(小车控制命令接收线程)package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.PausableThread;

/**
* Created by seth.yang on 2015/6/8.
*/
public class BlinkLED extends PausableThread {
    private GpioPinDigitalOutput pin;

    public BlinkLED (int pinIndex) {
      super (true);
      pin = GpioHelper.getDigitalOutputPin (pinIndex);
    }

    public boolean isBlinking () {
      return !paused;
    }

    public void blink () {
      proceed ();
    }

    @Override
    protected void doWork () {
      try {
            pin.high ();
            sleep (300);
            pin.low ();
            sleep (300);
      } catch (Exception ex) {
                        // process exception
      }
    }
}
摄像头这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。mjpg_streamer -i "input_raspicam.so-fps 15" -o "output_http.so -w /usr/www -p 8002"
拼装小车控制程序
小车动作控制部分介绍完了,可以将这些代码拼装成一台完整的小车了。我们的小车需要:l 两路舵机,用于控制摄像头的转动l 四路直流电机(实际上我只用了两路,记得吗,我将左右两侧的2个电机并联了)l 四路转向灯(实际上是也是两路,同上)l 一路照明LED灯(好吧,上面的介绍中没有,但这不影响整体代码)package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.Config;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.Rotate;

import java.io.*;

public class Car implements ServoListener {
    private static final int
            DIR_FORWARD = 1, DIR_BACKWARD = -1, DIR_STOP = 0,
            DIR_TURN_LEFT = -2, DIR_TURN_RIGHT = 2;

        /**
       * 小车当前运动状态(前进,后退或停止)
       */
    private int dir = DIR_STOP;
        /**
       * 小车上一次的运动状态
       */
    private Integer backup_dir = null;
        /**
       * 小车转动状态(左转,右转,停止)
       */
    private Rotate rotate;
        /**
       * 两路转向灯
       */
    private BlinkLED leftLed, rightLed;
        /**
       * 两路舵机
       */
    private Servo servo0, servo1;
        /**
       * 两路直流电机
       */
    private Motor left_front, right_front;
        /**
       * 摄像头
       */
    private Camera camera;
        /**
       * 配置文件
       */
    private Config config;
        /**
       * 前灯
       */
    private GpioPinDigitalOutput led;

    private GpioController gpio;
    private boolean shutdown = false;

    public Car (Config config) throws IOException, InterruptedException {
      this.config = config;
      setup ();
    }

        /**
       * 初始化
       */
    private void setup () throws IOException, InterruptedException {
                ... // 读取配置文件,初始化各个部件
    }

        /**
       * 使指定的LED灯闪烁
       */
    private void toggleBlinkLed (BlinkLED led) {
                ...
    }

        /**
       * 重置小车的各个状态
       */
    public void reset () throws InterruptedException {
      stop ();
      servo0.reset ();
      servo1.reset ();
      camera.close ();
    }

        /**
       * 销毁小车的各个部件,释放资源
       */
    public void dispose () throws InterruptedException {
                ...
    }

    public void toggleLeftBlink () {
      ...
    }

    public void toggleRightBlink () {
      ...
    }

    public void forward () throws InterruptedException {
                ...
    }

    public void backward () throws InterruptedException {
                ...
    }

    public void stop () throws InterruptedException {
                ...
    }

    public void toggleServoLeft () {
                ...
    }

    public void toggleServoRight () {
                ...
    }

    public void toggleServoUp () {
                ...
    }

    public void toggleServoDown () {
                ...
    }

    public void toggleTurnLeft () throws InterruptedException {
                ...
    }

    public void toggleTurnRight () throws InterruptedException {
                ...
    }

    public void toggleLED () {
      led.toggle ();
    }

    public void toggleCamera () throws IOException {
      if (!camera.isOpened ())
            camera.open ();
      else
            camera.close ();
    }

    public void servoLeft () {
      servo0.increase ();
    }

    public void servoRight () {
      servo0.decrease ();
    }

    public void stopHorizontalRotate () {
      servo0.stopRotate ();
    }

    public void servoUp () {
      servo1.decrease ();
    }

    public void servoDown () {
      servo1.increase ();
    }

    public void stopVerticalRotate () {
      servo1.stopRotate ();
    }

    public boolean isRotateUp () {
      return rotate.isRotateUp ();
    }

    public boolean isRotateLeft () {
      return rotate.isRotateLeft ();
    }

    public boolean isRotateDown () {
      return rotate.isRotateDown ();
    }

    public boolean isRotateRight () {
      return rotate.isRotateRight ();
    }
}值得一提的是控制转向。我们的小车是四驱的,并没有转向轮,那么如何转向呢?答案是差速转向:当左右两侧轮子的转速不同时,小车就不再沿直线运动了。设小车左轮的速度为VL,右轮的转速为VR,那么理论上:n 当VL = VR时,转弯半径为无穷大(直线)n 当VL > VR>0时,小车右转,转弯半径 > 轴距n 当VL > VR=0时,小车右转,转弯半径 = 轴距n 当 VR = -VL时,小车右转,转弯半径 = 0反之左转我们的转弯代码取其中一种(我取的是一侧速率为0的方式,这取决于电机的减速比和轮胎的抓地力,如果电机减速比较小或轮胎抓地力较小,建议取VR = -VL,进行原地转弯)...
// 一侧速率为0的转弯方式
private void turnLeft () throws InterruptedException {
        left_front.setSpeed (5);
        right_front.setSpeed (5);
        if (dir == DIR_BACKWARD) {
                left_forward ();
                right_pause ();
      
        } else {
                left_pause ();
                right_forward ();
        }
        leftLed.blink ();
}

// 原地转弯的方式
private void turnLeft () throws InterruptedException {
        left_forward ();
    right_backward ();
        leftLed.blink ();
}
...指令我们需要指令,来和小车的动作一一对应,枚举它就行了package org.dreamwork.smart.car.server.io;

public enum Command {
    DISPOSE (-2, false),
    QUIT (-1, false),

    STOP ( 0, false),
    FORWARD (1, false),
    BACKWARD (2, false),
    TURN_LEFT (3, false),
    TURN_RIGHT (4, false),

    TOGGLE_LED (5, false),
    TOGGLE_CAMERA (6, false),
    TOGGLE_LEFT_BLINK (7, false),
    TOGGLE_RIGHT_BLINK (8, false),

    STOP_VERTICAL_SERVO (9, false),
    STOP_HORIZONTAL_SERVO (10, false),

    SPEED (11, false),
    SERVO_UP (12, false),
    SERVO_RIGHT (13, false),
    SERVO_DOWN (14, false),
    SERVO_LEFT (15, false),

    LEFT_FORWARD (101, false),
    LEFT_BACKWARD (102, false),
    LEFT_PAUSE (103, false),
    RIGHT_FORWARD (104, false),
    RIGHT_BACKWARD (105, false),
    RIGHT_PAUSE (106, false),

    RESET (501, false)
    ;

    public final int code;
    public final boolean hasReturn;

    private Command (int code, boolean hasReturn) {
      this.code = code;
      this.hasReturn = hasReturn;
    }
}--- 呃,2楼还不够,代码贴的有点多了。。。

seth.yang 发表于 2015-7-1 14:51:29

本帖最后由 seth.yang 于 2015-7-1 16:41 编辑

-- 接楼上网络监听实际上,这部分和树莓派的GPIO控制无关,如果你对java的socket编程比较熟悉的,应该很容易看懂。想象一下我们的控制流程:1. 手机连接到树莓派2. 发送对应的控制指令3. 树莓派做出响应显而易见,手机需要知道树莓派的IP地址,才能够连接到树莓派进行小车的遥控。而树莓派也需要监听网络,等待指令。package org.dreamwork.smart.car.server.io;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server implements Runnable {
private ServerSocket server;
private String name;
private int port;

public Server (String name, int port) {
this.name = name;
this.port = port;
}

/**
* 监听网络
*/
public void bind () throws IOException {
server = new ServerSocket (port);
new Thread (this).start ();
}

public void unbind () throws IOException {
if (!server.isClosed ()) {
server.close ();
}
}

@Override
public void run () {
... // 初始化
while (!server.isClosed ()) {
try {
Socket socket = server.accept ();
// 将小车控制权交给新的线程,enjoy it!
Worker worker = new Worker (socket, car);
new Thread (runner).start ();
} catch (Exception ex) {
ex.printStackTrace ();
}
}
}
}
Server 负责监听并接受连接,Worker负责接受指令并响应。至此,小车已经可以工作了。等等,树莓派的IP地址是啥?我还要在手机先输入树莓派的IP,好麻烦啊~~~好吧,我们可以扩展一下网络部分的代码,让树莓派把自己的IP和端口广播出来。。。仅介绍思路,代码在https://github.com/seth-yang/remote-car上,org.dreamwork.smart.car.server.io.BroadcastService类 和 org.dreamwork.smart.car.server.io.NetworkUtil类思路如下:树莓派监听自己所有网卡绑定的网络的广播地址的某一个端口(比如8001),手机端程序向自己所能连接的所有网络的广播地址发送自己的IP,树莓派从广播地址读到手机IP后,通过TCP将自己的IP、摄像头端口及控制端口发送给手机。编写一个基于java swing的测试程序,测试一下我们的小车。


每个功能都分配一个单独的按钮,拆开来测试比较好。友情提示,测试的时候将小车架空会比较有爱写,像这样
动起来的效果:视频土豆上http://www.tudou.com/programs/view/CR1aPVNxkjI/http://www.tudou.com/programs/view/CR1aPVNxkjI/
--- TO BE CONTINUED ---下期预告树莓派WIFI小车java版(是)android 控制程序



seth.yang 发表于 2015-7-1 15:04:34

今天上传图片的份额用完了,明天上第四部分。

树老大 发表于 2015-7-1 15:19:30

精彩。感谢贡献。

seth.yang 发表于 2015-7-1 15:36:36

-- 接楼上

直流电机控制
直流电机是通过L298N控制板进行控制的。一块L298N控制板可以同时控制2路直流电机,其中,每路直流电机需要3根脚进行控制,一根使用PWM信号控制电机的转速,2路数字信号控制电机的转动状态,请参见下表
了解L298N的控制方式后,控制代码比较简单。package org.dreamwork.smart.car.server.component;
public class Motor {
    private GpioPinDigitalOutput pin0, pin1;
    private PWM pwm;
    private int speed = 3;

    public static final int MAX_SPEED = 5, MIN_SPEED = 0;

    public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
      this.pin0 = pin0;
      this.pin1 = pin1;
      this.pwm = pwm;
    }

    public int getSpeed () {
      return speed;
    }

    public void setSpeed (int speed) {
      if (speed < MIN_SPEED) speed = MIN_SPEED;
      if (speed > MAX_SPEED) speed = MAX_SPEED;
      this.speed = speed;
                // 将速度映射为PWM占空比
      pwm.setValue (.2f * speed);
    }

    public void forward () {
      pin0.high ();
      pin1.low ();
    }

    public void backward () {
      pin0.low ();
      pin1.high ();
    }

    public void stop () {
      pin0.low ();
      pin1.low ();
    }

    public void dispose () {
      pin0.low ();
      pin1.low ();
      pwm.setValue (0);
    }
}

增加转向灯
这部分对于小车运动控制来说,不是必须的,但可以增加趣味性,同时更加接近现实情况:当车辆转弯时转向灯同时闪烁,指示车辆转弯的方向。这部分的代码相当简单,就是控制一个LED灯的闪烁,唯一不同的事,采用线程,使得LED的闪烁不影响主线程(小车控制命令接收线程)package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.PausableThread;

/**
* Created by seth.yang on 2015/6/8.
*/
public class BlinkLED extends PausableThread {
    private GpioPinDigitalOutput pin;

    public BlinkLED (int pinIndex) {
      super (true);
      pin = GpioHelper.getDigitalOutputPin (pinIndex);
    }

    public boolean isBlinking () {
      return !paused;
    }

    public void blink () {
      proceed ();
    }

    @Override
    protected void doWork () {
      try {
            pin.high ();
            sleep (300);
            pin.low ();
            sleep (300);
      } catch (Exception ex) {
                        // process exception
      }
    }
}

摄像头
这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。
拼装小车控制程序
小车动作控制部分介绍完了,可以将这些代码拼装成一台完整的小车了。我们的小车需要:l 两路舵机,用于控制摄像头的转动l 四路直流电机(实际上我只用了两路,记得吗,我将左右两侧的2个电机并联了)l 四路转向灯(实际上是也是两路,同上)l 一路照明LED灯(好吧,上面的介绍中没有,但这不影响整体代码)
package org.dreamwork.smart.car.server.component;

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import org.apache.log4j.Logger;
import org.dreamwork.smart.car.server.util.Config;
import org.dreamwork.smart.car.server.util.GpioHelper;
import org.dreamwork.smart.car.server.util.Rotate;

import java.io.*;

public class Car implements ServoListener {
    private static final int
            DIR_FORWARD = 1, DIR_BACKWARD = -1, DIR_STOP = 0,
            DIR_TURN_LEFT = -2, DIR_TURN_RIGHT = 2;

        /**
       * 小车当前运动状态(前进,后退或停止)
       */
    private int dir = DIR_STOP;
        /**
       * 小车上一次的运动状态
       */
    private Integer backup_dir = null;
        /**
       * 小车转动状态(左转,右转,停止)
       */
    private Rotate rotate;
        /**
       * 两路转向灯
       */
    private BlinkLED leftLed, rightLed;
        /**
       * 两路舵机
       */
    private Servo servo0, servo1;
        /**
       * 两路直流电机
       */
    private Motor left_front, right_front;
        /**
       * 摄像头
       */
    private Camera camera;
        /**
       * 配置文件
       */
    private Config config;
        /**
       * 前灯
       */
    private GpioPinDigitalOutput led;

    private GpioController gpio;
    private boolean shutdown = false;

    public Car (Config config) throws IOException, InterruptedException {
      this.config = config;
      setup ();
    }

        /**
       * 初始化
       */
    private void setup () throws IOException, InterruptedException {
                ... // 读取配置文件,初始化各个部件
    }

        /**
       * 使指定的LED灯闪烁
       */
    private void toggleBlinkLed (BlinkLED led) {
                ...
    }

        /**
       * 重置小车的各个状态
       */
    public void reset () throws InterruptedException {
      stop ();
      servo0.reset ();
      servo1.reset ();
      camera.close ();
    }

        /**
       * 销毁小车的各个部件,释放资源
       */
    public void dispose () throws InterruptedException {
                ...
    }

    public void toggleLeftBlink () {
      ...
    }

    public void toggleRightBlink () {
      ...
    }

    public void forward () throws InterruptedException {
                ...
    }

    public void backward () throws InterruptedException {
                ...
    }

    public void stop () throws InterruptedException {
                ...
    }

    public void toggleServoLeft () {
                ...
    }

    public void toggleServoRight () {
                ...
    }

    public void toggleServoUp () {
                ...
    }

    public void toggleServoDown () {
                ...
    }

    public void toggleTurnLeft () throws InterruptedException {
                ...
    }

    public void toggleTurnRight () throws InterruptedException {
                ...
    }

    public void toggleLED () {
      led.toggle ();
    }

    public void toggleCamera () throws IOException {
      if (!camera.isOpened ())
            camera.open ();
      else
            camera.close ();
    }

    public void servoLeft () {
      servo0.increase ();
    }

    public void servoRight () {
      servo0.decrease ();
    }

    public void stopHorizontalRotate () {
      servo0.stopRotate ();
    }

    public void servoUp () {
      servo1.decrease ();
    }

    public void servoDown () {
      servo1.increase ();
    }

    public void stopVerticalRotate () {
      servo1.stopRotate ();
    }

    public boolean isRotateUp () {
      return rotate.isRotateUp ();
    }

    public boolean isRotateLeft () {
      return rotate.isRotateLeft ();
    }

    public boolean isRotateDown () {
      return rotate.isRotateDown ();
    }

    public boolean isRotateRight () {
      return rotate.isRotateRight ();
    }
}代码较长,截取了其中public的方法,抱歉注释不多,不过看名字应该能看懂方法的含义。
值得一提的是控制转向。我们的小车是四驱的,并没有转向轮,那么如何转向呢?答案是差速转向:当左右两侧轮子的转速不同时,小车就不再沿直线运动了。设小车左轮的速度为VL,右轮的转速为VR,那么理论上:n 当VL = VR时,转弯半径为无穷大(直线)n 当VL > VR>0时,小车右转,转弯半径 > 轴距n 当VL > VR=0时,小车右转,转弯半径 = 轴距n 当 VR = -VL时,小车右转,转弯半径 = 0反之左转我们的转弯代码取其中一种(我取的是一侧速率为0的方式,这取决于电机的减速比和轮胎的抓地力,如果电机减速比较小或轮胎抓地力较小,建议取VR = -VL,进行原地转弯)...
// 一侧速率为0的转弯方式
private void turnLeft () throws InterruptedException {
        left_front.setSpeed (5);
        right_front.setSpeed (5);
        if (dir == DIR_BACKWARD) {
                left_forward ();
                right_pause ();
      
        } else {
                left_pause ();
                right_forward ();
        }
        leftLed.blink ();
}

// 原地转弯的方式
private void turnLeft () throws InterruptedException {
        left_forward ();
    right_backward ();
        leftLed.blink ();
}
...

指令
我们需要指令,来和小车的动作一一对应,枚举它就行了package org.dreamwork.smart.car.server.io;

public enum Command {
    DISPOSE (-2, false),
    QUIT (-1, false),

    STOP ( 0, false),
    FORWARD (1, false),
    BACKWARD (2, false),
    TURN_LEFT (3, false),
    TURN_RIGHT (4, false),

    TOGGLE_LED (5, false),
    TOGGLE_CAMERA (6, false),
    TOGGLE_LEFT_BLINK (7, false),
    TOGGLE_RIGHT_BLINK (8, false),

    STOP_VERTICAL_SERVO (9, false),
    STOP_HORIZONTAL_SERVO (10, false),

    SPEED (11, false),
    SERVO_UP (12, false),
    SERVO_RIGHT (13, false),
    SERVO_DOWN (14, false),
    SERVO_LEFT (15, false),

    LEFT_FORWARD (101, false),
    LEFT_BACKWARD (102, false),
    LEFT_PAUSE (103, false),
    RIGHT_FORWARD (104, false),
    RIGHT_BACKWARD (105, false),
    RIGHT_PAUSE (106, false),

    RESET (501, false)
    ;

    public final int code;
    public final boolean hasReturn;

    private Command (int code, boolean hasReturn) {
      this.code = code;
      this.hasReturn = hasReturn;
    }
}
-- 看来2楼还是无法完全放下全部内容,代码贴的有点多,呵呵,下面还有一楼,介绍本篇剩余的内容,不要走开哦






seth.yang 发表于 2015-7-1 16:02:48

本篇还剩下2部分发不到这个帖子里了,是要新开贴吗?

树老大 发表于 2015-7-1 16:20:27

seth.yang 发表于 2015-7-1 16:02
本篇还剩下2部分发不到这个帖子里了,是要新开贴吗?

重新开就行

seth.yang 发表于 2015-7-1 16:37:38

呃。。。全出来了,老大把重复的内容删除了吧。麻烦了。
页: [1] 2
查看完整版本: 树莓派WIFI小车java版(三)小车java控制程序