万博网页版登陆页派论坛

万博网页版登陆页派WIFI小车java版(三)小车java控制程序

seth.yang 发表于 2015-7-1 14:03:24 | 显示全部楼层 |阅读模式
本帖最后由 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终端中,使用命令
  1. git clone https://github.com/seth-yang/remote-car.git
复制代码
来获取整个工程。

java线程基础

这部分是给对java线程控制不太熟悉的同学看的,若你对java的线程控制很熟悉的话,请直接跳到下一节。

我们需要一个可暂停的线程,线程外部可控制该线程的状态(运行、暂停、退出)。思路很简单,线程监视一个变量,若设置,则等待,否则继续执行线程逻辑,代码如下:
  1. package org.dreamwork.smart.car.server.util;

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

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

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

  21.         /**
  22.          * 恢复执行线程
  23.          */
  24.     public void proceed () {
  25.         synchronized (locker) {
  26.             paused = false;
  27.             locker.notifyAll ();
  28.         }
  29.     }

  30.         /**
  31.          * 暂停线程
  32.          */
  33.     public void pause () {
  34.         paused = true;
  35.     }

  36.     public boolean isPaused () {
  37.         return paused;
  38.     }

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

  49.     @Override
  50.     public synchronized void start () {
  51.         if (!running) {
  52.             running = true;
  53.             super.start ();
  54.         }
  55.     }

  56.     @Override
  57.     public void run () {
  58.         while (running) {
  59.             while (paused) {
  60.                 synchronized (locker) {
  61.                     try {
  62.                         locker.wait ();
  63.                     } catch (InterruptedException e) {
  64.                         e.printStackTrace ();
  65.                     }
  66.                 }
  67.             }

  68.             doWork ();
  69.         }
  70.     }
  71. }
复制代码

关于pi4j的简单介绍
Pi4j 提供的万博网页版登陆页派IO脚控制的API,入门还是先对简单的,官网提供了一些入门代码,我们来看个最简单的。
  1. // 初始化GPIO环境
  2. GpioController gpio = GpioFactory.getInstance();
  3. // 获取一个指定的GPIO脚
  4. GpioPinDigitalOutput pin =
  5.      gpio.provisionDigitalOutputPin(
  6.           RaspiPin.GPIO_01,   // GPIO 脚序号
  7.           "MyLED",             // GPIO脚名称
  8.           PinState.LOW        // 初始状态
  9.      );
  10. // 控制连接GPIO脚的LED闪烁10次
  11. for (int i = 0; i < 10; i ++) {
  12. pin.high ();
  13. Thread.sleep (1000);
  14. pin.low ();
  15. Thread.sleep (1000);
  16. }
复制代码
很简单,是不是。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的入门例子,这是一个呼吸灯的代码。

  1. // 获取I2C 总线对象
  2. I2CBus bus = I2CFactory.getInstance(I2CBus.BUS_1);
  3. // 定义 PWM 频率
  4. BigDecimal frequency = new BigDecimal("50");
  5. // 初始化PCA9685驱动
  6. PCA9685GpioProvider provider = new PCA9685GpioProvider(
  7.       bus,            // I2C 总线对象
  8.       0x40,           // I2C 总线地址 对于PCA9685而言,固定为 0x40
  9.       frequency       // PWM 频率
  10. );
  11. Pin led = PCA9685Pin.ALL[0];    // PCA9685 PWM 0脚
  12. for (int i = 0; i < 10; i ++) {
  13. for (int j = 0; j < 4096; j += 10) {
  14.     if (j < 4096) {
  15.         // 从第0个tick开始到第j个tick结束
  16.         provider.setPWM (led, 0, j);   
  17.         Thread.sleep (1);
  18.     }
  19. }
  20. for (int j = 4096; j > 0; j -= 10) {
  21.     if (j > 0) {
  22.         provider.setPWM (led, 0, j);
  23.         Thread.sleep (1);
  24.     }
  25. }
  26. }
复制代码
其中,provider.setPWM (led, 0, j)这句不太好理解,这里简单解释一下,详细内容请参阅官网文档
PCA9685将每个PWM周期等分为4096个时刻(更确切点,称为tick),该方法的第二个参数为高电平的起始tick,第三个参数为高电平的结束时刻,以这种形式来定义PWM的占空比。例如,我们定义占空比为25%的PWM,那么,该方法的参数为:
  1. provider.setPWM (led, 0, 1024)
复制代码
它的PWM波形类似
pwm-1.png

如果将参数改为
  1. provider.setPWM (led, 1024, 2048)
复制代码
这同样是一个25%占空比的PWM,不过它的波形为
pwm-2.png

好了,我们点到为止,毕竟这不是详细介绍PWM的。
根据以上的思路,我们使用java语言为PWM抽象一个类出来,接口如下:
  1. public class PWM {
  2.     private float value;
  3.         /**
  4.          *  构造函数,指定PWM的引脚和频率
  5.          */
  6. public PWM (int pin, int frequency) {
  7.    ...
  8. }
  9.         /**
  10.          * 设置占空比,将原始int, int方式转化为更直观的百分比形式
  11.          */
  12. public void setValue (float value) {
  13.      this.value = value;
  14.      ... // 将百分比映射成 0~4095之间的值
  15. }
  16.         /**
  17.          * 获取当前的占空比
  18.          */
  19. public float getValue () {
  20.     return value;
  21. }
  22. }
复制代码

舵机控制
舵机的控制信号周期为20ms的脉宽调制(PWM)信号,其中脉冲宽度从0.5-2.5ms,相对应的舵盘位置为0-180度,呈线性变化。也就是说,给他提供一定的脉宽,它的输出轴就会保持一定对应角度上,无论外界转矩怎么改变,直到给它提供一个另外宽度的脉冲信号,它才会改变输出角度到新的对应位置上,如下图所示
pwm.png
不难看出,只要给相对应的脉冲宽度(即占空比),就能精确控制舵机停留的角度,根据这个思路,我们也抽象出一个java 对象来表示舵机 (原型,实际代码比这个复杂)
  1. /**
  2. * 基于 PCA9685 扩展板的 舵机对象.
  3. */
  4. public class Servo extends PWM implements TimeoutListener {
  5.     public static enum Direction {
  6.         INCREMENT, DECREMENT
  7.     }

  8.         /**
  9.          * 最小的有效脉宽
  10.          */
  11.     public static final int SERVO_DURATION_MIN = 500;
  12.         /**
  13.          * 中位的脉宽值
  14.          */
  15.     public static final int SERVO_DURATION_NEUTRAL = 1500;
  16.         /**
  17.          * 最大有效脉宽
  18.          */
  19.     public static final int SERVO_DURATION_MAX = 2500;

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

  28.     /**
  29.          * 舵机转动方向
  30.          */
  31.     private Direction dir = Direction.INCREMENT;

  32.         /**
  33.          * 事件处理器
  34.          */
  35.     private List<ServoListener> list = new ArrayList<ServoListener> ();

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

  39.         /**
  40.          * 设定舵机的角度
  41.          */
  42.     public void set (int angle) throws InterruptedException {
  43.         if (angle < minAngle)
  44.             angle = minAngle;
  45.         if (angle > maxAngle)
  46.             angle = maxAngle;
  47.         this.angle = angle;
  48.         double tmp = angle + 90;
  49.         // 将角度计算为脉宽
  50.         double duration = SERVO_DURATION_MIN + 100 * tmp / 9;
  51.         if (duration > SERVO_DURATION_MAX)
  52.              duration = SERVO_DURATION_MAX;
  53.         else if (duration < SERVO_DURATION_MIN)
  54.              duration = SERVO_DURATION_MIN;
  55.         provider.setPwm (PCA9685Pin.ALL [pinIndex], (int) duration);
  56.     }

  57.         /**
  58.          * 获取舵机当前停留的角度
  59.          */
  60.     public int get () {
  61.         return angle;
  62.     }

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

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

更多图片 小图 大图
组图打开中,请稍候......

评分

+ 5
很给力!

查看全部评分

seth.yang  楼主| 发表于 2015-7-1 15:46:23 | 显示全部楼层
-- 接楼上

直流电机控制
直流电机是通过L298N控制板进行控制的。一块L298N控制板可以同时控制2路直流电机,其中,每路直流电机需要3根脚进行控制,一根使用PWM信号控制电机的转速,2路数字信号控制电机的转动状态,请参见下表

L298N

L298N
了解L298N的控制方式后,控制代码比较简单。
  1. package org.dreamwork.smart.car.server.component;
  2. public class Motor {
  3.     private GpioPinDigitalOutput pin0, pin1;
  4.     private PWM pwm;
  5.     private int speed = 3;

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

  7.     public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
  8.         this.pin0 = pin0;
  9.         this.pin1 = pin1;
  10.         this.pwm = pwm;
  11.     }

  12.     public int getSpeed () {
  13.         return speed;
  14.     }

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

  22.     public void forward () {
  23.         pin0.high ();
  24.         pin1.low ();
  25.     }

  26.     public void backward () {
  27.         pin0.low ();
  28.         pin1.high ();
  29.     }

  30.     public void stop () {
  31.         pin0.low ();
  32.         pin1.low ();
  33.     }

  34.     public void dispose () {
  35.         pin0.low ();
  36.         pin1.low ();
  37.         pwm.setValue (0);
  38.     }
  39. }
复制代码

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

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

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

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

  15.     public boolean isBlinking () {
  16.         return !paused;
  17.     }

  18.     public void blink () {
  19.         proceed ();
  20.     }

  21.     @Override
  22.     protected void doWork () {
  23.         try {
  24.             pin.high ();
  25.             sleep (300);
  26.             pin.low ();
  27.             sleep (300);
  28.         } catch (Exception ex) {
  29.                         // process exception
  30.         }
  31.     }
  32. }
复制代码

摄像头
这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。

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

  2. import com.pi4j.io.gpio.GpioController;
  3. import com.pi4j.io.gpio.GpioFactory;
  4. import com.pi4j.io.gpio.GpioPinDigitalOutput;
  5. import org.apache.log4j.Logger;
  6. import org.dreamwork.smart.car.server.util.Config;
  7. import org.dreamwork.smart.car.server.util.GpioHelper;
  8. import org.dreamwork.smart.car.server.util.Rotate;

  9. import java.io.*;

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

  14.         /**
  15.          * 小车当前运动状态(前进,后退或停止)
  16.          */
  17.     private int dir = DIR_STOP;
  18.         /**
  19.          * 小车上一次的运动状态
  20.          */
  21.     private Integer backup_dir = null;
  22.         /**
  23.          * 小车转动状态(左转,右转,停止)
  24.          */
  25.     private Rotate rotate;
  26.         /**
  27.          * 两路转向灯
  28.          */
  29.     private BlinkLED leftLed, rightLed;
  30.         /**
  31.          * 两路舵机
  32.          */
  33.     private Servo servo0, servo1;
  34.         /**
  35.          * 两路直流电机
  36.          */
  37.     private Motor left_front, right_front;
  38.         /**
  39.          * 摄像头
  40.          */
  41.     private Camera camera;
  42.         /**
  43.          * 配置文件
  44.          */
  45.     private Config config;
  46.         /**
  47.          * 前灯
  48.          */
  49.     private GpioPinDigitalOutput led;

  50.     private GpioController gpio;
  51.     private boolean shutdown = false;

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

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

  62.         /**
  63.          * 使指定的LED灯闪烁
  64.          */
  65.     private void toggleBlinkLed (BlinkLED led) {
  66.                 ...
  67.     }

  68.         /**
  69.          * 重置小车的各个状态
  70.          */
  71.     public void reset () throws InterruptedException {
  72.         stop ();
  73.         servo0.reset ();
  74.         servo1.reset ();
  75.         camera.close ();
  76.     }

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

  83.     public void toggleLeftBlink () {
  84.         ...
  85.     }

  86.     public void toggleRightBlink () {
  87.         ...
  88.     }

  89.     public void forward () throws InterruptedException {
  90.                 ...
  91.     }

  92.     public void backward () throws InterruptedException {
  93.                 ...
  94.     }

  95.     public void stop () throws InterruptedException {
  96.                 ...
  97.     }

  98.     public void toggleServoLeft () {
  99.                 ...
  100.     }

  101.     public void toggleServoRight () {
  102.                 ...
  103.     }

  104.     public void toggleServoUp () {
  105.                 ...
  106.     }

  107.     public void toggleServoDown () {
  108.                 ...
  109.     }

  110.     public void toggleTurnLeft () throws InterruptedException {
  111.                 ...
  112.     }

  113.     public void toggleTurnRight () throws InterruptedException {
  114.                 ...
  115.     }

  116.     public void toggleLED () {
  117.         led.toggle ();
  118.     }

  119.     public void toggleCamera () throws IOException {
  120.         if (!camera.isOpened ())
  121.             camera.open ();
  122.         else
  123.             camera.close ();
  124.     }

  125.     public void servoLeft () {
  126.         servo0.increase ();
  127.     }

  128.     public void servoRight () {
  129.         servo0.decrease ();
  130.     }

  131.     public void stopHorizontalRotate () {
  132.         servo0.stopRotate ();
  133.     }

  134.     public void servoUp () {
  135.         servo1.decrease ();
  136.     }

  137.     public void servoDown () {
  138.         servo1.increase ();
  139.     }

  140.     public void stopVerticalRotate () {
  141.         servo1.stopRotate ();
  142.     }

  143.     public boolean isRotateUp () {
  144.         return rotate.isRotateUp ();
  145.     }

  146.     public boolean isRotateLeft () {
  147.         return rotate.isRotateLeft ();
  148.     }

  149.     public boolean isRotateDown () {
  150.         return rotate.isRotateDown ();
  151.     }

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

  16. // 原地转弯的方式
  17. private void turnLeft () throws InterruptedException {
  18.         left_forward ();
  19.     right_backward ();
  20.         leftLed.blink ();
  21. }
  22. ...
复制代码
以上代码二选一,不能同时出现在程序中,否则编译时将出错

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

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

  5.     STOP ( 0, false),
  6.     FORWARD (1, false),
  7.     BACKWARD (2, false),
  8.     TURN_LEFT (3, false),
  9.     TURN_RIGHT (4, false),

  10.     TOGGLE_LED (5, false),
  11.     TOGGLE_CAMERA (6, false),
  12.     TOGGLE_LEFT_BLINK (7, false),
  13.     TOGGLE_RIGHT_BLINK (8, false),

  14.     STOP_VERTICAL_SERVO (9, false),
  15.     STOP_HORIZONTAL_SERVO (10, false),

  16.     SPEED (11, false),
  17.     SERVO_UP (12, false),
  18.     SERVO_RIGHT (13, false),
  19.     SERVO_DOWN (14, false),
  20.     SERVO_LEFT (15, false),

  21.     LEFT_FORWARD (101, false),
  22.     LEFT_BACKWARD (102, false),
  23.     LEFT_PAUSE (103, false),
  24.     RIGHT_FORWARD (104, false),
  25.     RIGHT_BACKWARD (105, false),
  26.     RIGHT_PAUSE (106, false),

  27.     RESET (501, false)
  28.     ;

  29.     public final int code;
  30.     public final boolean hasReturn;

  31.     private Command (int code, boolean hasReturn) {
  32.         this.code = code;
  33.         this.hasReturn = hasReturn;
  34.     }
  35. }
复制代码

-- 字数有到了。。。下面还有一楼,介绍剩下的内容,不要走开哦



wire-4.png
seth.yang  楼主| 发表于 2015-7-1 14:44:40 | 显示全部楼层
-- 接楼上的内容

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

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

  7.     public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
  8.         this.pin0 = pin0;
  9.         this.pin1 = pin1;
  10.         this.pwm = pwm;
  11.     }

  12.     public int getSpeed () {
  13.         return speed;
  14.     }

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

  22.     public void forward () {
  23.         pin0.high ();
  24.         pin1.low ();
  25.     }

  26.     public void backward () {
  27.         pin0.low ();
  28.         pin1.high ();
  29.     }

  30.     public void stop () {
  31.         pin0.low ();
  32.         pin1.low ();
  33.     }

  34.     public void dispose () {
  35.         pin0.low ();
  36.         pin1.low ();
  37.         pwm.setValue (0);
  38.     }
  39. }
复制代码

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

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

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

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

  15.     public boolean isBlinking () {
  16.         return !paused;
  17.     }

  18.     public void blink () {
  19.         proceed ();
  20.     }

  21.     @Override
  22.     protected void doWork () {
  23.         try {
  24.             pin.high ();
  25.             sleep (300);
  26.             pin.low ();
  27.             sleep (300);
  28.         } catch (Exception ex) {
  29.                         // process exception
  30.         }
  31.     }
  32. }
复制代码

摄像头
这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。
  1. mjpg_streamer -i "input_raspicam.so  -fps 15" -o "output_http.so -w /usr/www -p 8002"
复制代码

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

  2. import com.pi4j.io.gpio.GpioController;
  3. import com.pi4j.io.gpio.GpioFactory;
  4. import com.pi4j.io.gpio.GpioPinDigitalOutput;
  5. import org.apache.log4j.Logger;
  6. import org.dreamwork.smart.car.server.util.Config;
  7. import org.dreamwork.smart.car.server.util.GpioHelper;
  8. import org.dreamwork.smart.car.server.util.Rotate;

  9. import java.io.*;

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

  14.         /**
  15.          * 小车当前运动状态(前进,后退或停止)
  16.          */
  17.     private int dir = DIR_STOP;
  18.         /**
  19.          * 小车上一次的运动状态
  20.          */
  21.     private Integer backup_dir = null;
  22.         /**
  23.          * 小车转动状态(左转,右转,停止)
  24.          */
  25.     private Rotate rotate;
  26.         /**
  27.          * 两路转向灯
  28.          */
  29.     private BlinkLED leftLed, rightLed;
  30.         /**
  31.          * 两路舵机
  32.          */
  33.     private Servo servo0, servo1;
  34.         /**
  35.          * 两路直流电机
  36.          */
  37.     private Motor left_front, right_front;
  38.         /**
  39.          * 摄像头
  40.          */
  41.     private Camera camera;
  42.         /**
  43.          * 配置文件
  44.          */
  45.     private Config config;
  46.         /**
  47.          * 前灯
  48.          */
  49.     private GpioPinDigitalOutput led;

  50.     private GpioController gpio;
  51.     private boolean shutdown = false;

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

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

  62.         /**
  63.          * 使指定的LED灯闪烁
  64.          */
  65.     private void toggleBlinkLed (BlinkLED led) {
  66.                 ...
  67.     }

  68.         /**
  69.          * 重置小车的各个状态
  70.          */
  71.     public void reset () throws InterruptedException {
  72.         stop ();
  73.         servo0.reset ();
  74.         servo1.reset ();
  75.         camera.close ();
  76.     }

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

  83.     public void toggleLeftBlink () {
  84.         ...
  85.     }

  86.     public void toggleRightBlink () {
  87.         ...
  88.     }

  89.     public void forward () throws InterruptedException {
  90.                 ...
  91.     }

  92.     public void backward () throws InterruptedException {
  93.                 ...
  94.     }

  95.     public void stop () throws InterruptedException {
  96.                 ...
  97.     }

  98.     public void toggleServoLeft () {
  99.                 ...
  100.     }

  101.     public void toggleServoRight () {
  102.                 ...
  103.     }

  104.     public void toggleServoUp () {
  105.                 ...
  106.     }

  107.     public void toggleServoDown () {
  108.                 ...
  109.     }

  110.     public void toggleTurnLeft () throws InterruptedException {
  111.                 ...
  112.     }

  113.     public void toggleTurnRight () throws InterruptedException {
  114.                 ...
  115.     }

  116.     public void toggleLED () {
  117.         led.toggle ();
  118.     }

  119.     public void toggleCamera () throws IOException {
  120.         if (!camera.isOpened ())
  121.             camera.open ();
  122.         else
  123.             camera.close ();
  124.     }

  125.     public void servoLeft () {
  126.         servo0.increase ();
  127.     }

  128.     public void servoRight () {
  129.         servo0.decrease ();
  130.     }

  131.     public void stopHorizontalRotate () {
  132.         servo0.stopRotate ();
  133.     }

  134.     public void servoUp () {
  135.         servo1.decrease ();
  136.     }

  137.     public void servoDown () {
  138.         servo1.increase ();
  139.     }

  140.     public void stopVerticalRotate () {
  141.         servo1.stopRotate ();
  142.     }

  143.     public boolean isRotateUp () {
  144.         return rotate.isRotateUp ();
  145.     }

  146.     public boolean isRotateLeft () {
  147.         return rotate.isRotateLeft ();
  148.     }

  149.     public boolean isRotateDown () {
  150.         return rotate.isRotateDown ();
  151.     }

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

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

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

  5.     STOP ( 0, false),
  6.     FORWARD (1, false),
  7.     BACKWARD (2, false),
  8.     TURN_LEFT (3, false),
  9.     TURN_RIGHT (4, false),

  10.     TOGGLE_LED (5, false),
  11.     TOGGLE_CAMERA (6, false),
  12.     TOGGLE_LEFT_BLINK (7, false),
  13.     TOGGLE_RIGHT_BLINK (8, false),

  14.     STOP_VERTICAL_SERVO (9, false),
  15.     STOP_HORIZONTAL_SERVO (10, false),

  16.     SPEED (11, false),
  17.     SERVO_UP (12, false),
  18.     SERVO_RIGHT (13, false),
  19.     SERVO_DOWN (14, false),
  20.     SERVO_LEFT (15, false),

  21.     LEFT_FORWARD (101, false),
  22.     LEFT_BACKWARD (102, false),
  23.     LEFT_PAUSE (103, false),
  24.     RIGHT_FORWARD (104, false),
  25.     RIGHT_BACKWARD (105, false),
  26.     RIGHT_PAUSE (106, false),

  27.     RESET (501, false)
  28.     ;

  29.     public final int code;
  30.     public final boolean hasReturn;

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

seth.yang  楼主| 发表于 2015-7-1 14:51:29 | 显示全部楼层
本帖最后由 seth.yang 于 2015-7-1 16:41 编辑

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

  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;

  5. public class Server implements Runnable {
  6. private ServerSocket server;
  7. private String name;
  8. private int port;

  9. public Server (String name, int port) {
  10. this.name = name;
  11. this.port = port;
  12. }

  13. /**
  14. * 监听网络
  15. */
  16. public void bind () throws IOException {
  17. server = new ServerSocket (port);
  18. new Thread (this).start ();
  19. }

  20. public void unbind () throws IOException {
  21. if (!server.isClosed ()) {
  22. server.close ();
  23. }
  24. }

  25. @Override
  26. public void run () {
  27. ... // 初始化
  28. while (!server.isClosed ()) {
  29. try {
  30. Socket socket = server.accept ();
  31. // 将小车控制权交给新的线程,enjoy it!
  32. Worker worker = new Worker (socket, car);
  33. new Thread (runner).start ();
  34. } catch (Exception ex) {
  35. ex.printStackTrace ();
  36. }
  37. }
  38. }
  39. }
复制代码

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的测试程序,测试一下我们的小车。

client-1.png
client-2.png
每个功能都分配一个单独的按钮,拆开来测试比较好。
友情提示,测试的时候将小车架空会比较有爱写,像这样
car-3.jpg

动起来的效果:

--- 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路数字信号控制电机的转动状态,请参见下表
wire-4.png

了解L298N的控制方式后,控制代码比较简单。
  1. package org.dreamwork.smart.car.server.component;
  2. public class Motor {
  3.     private GpioPinDigitalOutput pin0, pin1;
  4.     private PWM pwm;
  5.     private int speed = 3;

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

  7.     public Motor (GpioPinDigitalOutput pin0, GpioPinDigitalOutput pin1, PWM pwm) {
  8.         this.pin0 = pin0;
  9.         this.pin1 = pin1;
  10.         this.pwm = pwm;
  11.     }

  12.     public int getSpeed () {
  13.         return speed;
  14.     }

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

  22.     public void forward () {
  23.         pin0.high ();
  24.         pin1.low ();
  25.     }

  26.     public void backward () {
  27.         pin0.low ();
  28.         pin1.high ();
  29.     }

  30.     public void stop () {
  31.         pin0.low ();
  32.         pin1.low ();
  33.     }

  34.     public void dispose () {
  35.         pin0.low ();
  36.         pin1.low ();
  37.         pwm.setValue (0);
  38.     }
  39. }
复制代码


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

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

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

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

  15.     public boolean isBlinking () {
  16.         return !paused;
  17.     }

  18.     public void blink () {
  19.         proceed ();
  20.     }

  21.     @Override
  22.     protected void doWork () {
  23.         try {
  24.             pin.high ();
  25.             sleep (300);
  26.             pin.low ();
  27.             sleep (300);
  28.         } catch (Exception ex) {
  29.                         // process exception
  30.         }
  31.     }
  32. }
复制代码


摄像头
这个和java关系不大,和GPIO也没关系。通过java调用mjpg-streamer就行了。

拼装小车控制程序

小车动作控制部分介绍完了,可以将这些代码拼装成一台完整的小车了。
我们的小车需要:
l 两路舵机,用于控制摄像头的转动
l 四路直流电机(实际上我只用了两路,记得吗,我将左右两侧的2个电机并联了)
l 四路转向灯(实际上是也是两路,同上)
l 一路照明LED灯(好吧,上面的介绍中没有,但这不影响整体代码)

  1. package org.dreamwork.smart.car.server.component;

  2. import com.pi4j.io.gpio.GpioController;
  3. import com.pi4j.io.gpio.GpioFactory;
  4. import com.pi4j.io.gpio.GpioPinDigitalOutput;
  5. import org.apache.log4j.Logger;
  6. import org.dreamwork.smart.car.server.util.Config;
  7. import org.dreamwork.smart.car.server.util.GpioHelper;
  8. import org.dreamwork.smart.car.server.util.Rotate;

  9. import java.io.*;

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

  14.         /**
  15.          * 小车当前运动状态(前进,后退或停止)
  16.          */
  17.     private int dir = DIR_STOP;
  18.         /**
  19.          * 小车上一次的运动状态
  20.          */
  21.     private Integer backup_dir = null;
  22.         /**
  23.          * 小车转动状态(左转,右转,停止)
  24.          */
  25.     private Rotate rotate;
  26.         /**
  27.          * 两路转向灯
  28.          */
  29.     private BlinkLED leftLed, rightLed;
  30.         /**
  31.          * 两路舵机
  32.          */
  33.     private Servo servo0, servo1;
  34.         /**
  35.          * 两路直流电机
  36.          */
  37.     private Motor left_front, right_front;
  38.         /**
  39.          * 摄像头
  40.          */
  41.     private Camera camera;
  42.         /**
  43.          * 配置文件
  44.          */
  45.     private Config config;
  46.         /**
  47.          * 前灯
  48.          */
  49.     private GpioPinDigitalOutput led;

  50.     private GpioController gpio;
  51.     private boolean shutdown = false;

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

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

  62.         /**
  63.          * 使指定的LED灯闪烁
  64.          */
  65.     private void toggleBlinkLed (BlinkLED led) {
  66.                 ...
  67.     }

  68.         /**
  69.          * 重置小车的各个状态
  70.          */
  71.     public void reset () throws InterruptedException {
  72.         stop ();
  73.         servo0.reset ();
  74.         servo1.reset ();
  75.         camera.close ();
  76.     }

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

  83.     public void toggleLeftBlink () {
  84.         ...
  85.     }

  86.     public void toggleRightBlink () {
  87.         ...
  88.     }

  89.     public void forward () throws InterruptedException {
  90.                 ...
  91.     }

  92.     public void backward () throws InterruptedException {
  93.                 ...
  94.     }

  95.     public void stop () throws InterruptedException {
  96.                 ...
  97.     }

  98.     public void toggleServoLeft () {
  99.                 ...
  100.     }

  101.     public void toggleServoRight () {
  102.                 ...
  103.     }

  104.     public void toggleServoUp () {
  105.                 ...
  106.     }

  107.     public void toggleServoDown () {
  108.                 ...
  109.     }

  110.     public void toggleTurnLeft () throws InterruptedException {
  111.                 ...
  112.     }

  113.     public void toggleTurnRight () throws InterruptedException {
  114.                 ...
  115.     }

  116.     public void toggleLED () {
  117.         led.toggle ();
  118.     }

  119.     public void toggleCamera () throws IOException {
  120.         if (!camera.isOpened ())
  121.             camera.open ();
  122.         else
  123.             camera.close ();
  124.     }

  125.     public void servoLeft () {
  126.         servo0.increase ();
  127.     }

  128.     public void servoRight () {
  129.         servo0.decrease ();
  130.     }

  131.     public void stopHorizontalRotate () {
  132.         servo0.stopRotate ();
  133.     }

  134.     public void servoUp () {
  135.         servo1.decrease ();
  136.     }

  137.     public void servoDown () {
  138.         servo1.increase ();
  139.     }

  140.     public void stopVerticalRotate () {
  141.         servo1.stopRotate ();
  142.     }

  143.     public boolean isRotateUp () {
  144.         return rotate.isRotateUp ();
  145.     }

  146.     public boolean isRotateLeft () {
  147.         return rotate.isRotateLeft ();
  148.     }

  149.     public boolean isRotateDown () {
  150.         return rotate.isRotateDown ();
  151.     }

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

  16. // 原地转弯的方式
  17. private void turnLeft () throws InterruptedException {
  18.         left_forward ();
  19.     right_backward ();
  20.         leftLed.blink ();
  21. }
  22. ...
复制代码


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

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

  5.     STOP ( 0, false),
  6.     FORWARD (1, false),
  7.     BACKWARD (2, false),
  8.     TURN_LEFT (3, false),
  9.     TURN_RIGHT (4, false),

  10.     TOGGLE_LED (5, false),
  11.     TOGGLE_CAMERA (6, false),
  12.     TOGGLE_LEFT_BLINK (7, false),
  13.     TOGGLE_RIGHT_BLINK (8, false),

  14.     STOP_VERTICAL_SERVO (9, false),
  15.     STOP_HORIZONTAL_SERVO (10, false),

  16.     SPEED (11, false),
  17.     SERVO_UP (12, false),
  18.     SERVO_RIGHT (13, false),
  19.     SERVO_DOWN (14, false),
  20.     SERVO_LEFT (15, false),

  21.     LEFT_FORWARD (101, false),
  22.     LEFT_BACKWARD (102, false),
  23.     LEFT_PAUSE (103, false),
  24.     RIGHT_FORWARD (104, false),
  25.     RIGHT_BACKWARD (105, false),
  26.     RIGHT_PAUSE (106, false),

  27.     RESET (501, false)
  28.     ;

  29.     public final int code;
  30.     public final boolean hasReturn;

  31.     private Command (int code, boolean hasReturn) {
  32.         this.code = code;
  33.         this.hasReturn = hasReturn;
  34.     }
  35. }
复制代码

-- 看来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 | 显示全部楼层
呃。。。全出来了,老大把重复的内容删除了吧。麻烦了。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版 | Archiver | 万博网页版登陆页派论坛 ( 粤ICP备15075382号-1 )