Browse Source

init pro
Signed-off-by: Caner

Caner 3 years ago
parent
commit
69640c404d
6 changed files with 432 additions and 1 deletions
  1. 7 1
      README.md
  2. 120 0
      index.js
  3. 147 0
      lib/Pwm.js
  4. 71 0
      lib/audioSource.js
  5. 68 0
      lib/videoSource.js
  6. 19 0
      package.json

+ 7 - 1
README.md

@@ -1,3 +1,9 @@
 # Client_For_Car
 # Client_For_Car
 
 
-车端
+### 不在使用 python
+
+### 同样摇杆控制
+
+### 视频采用 webrtc 传输
+
+### 通信服务 express,视频服务 socket,建立在控制端以减少车端视频解析压力

+ 120 - 0
index.js

@@ -0,0 +1,120 @@
+
+// wrt + socket + contrl
+const io = require("socket.io-client")
+const RTCVideoSource = require('./lib/videoSource')
+const RTCAudioSource = require('./lib/audioSource')
+//const PWM = require('./lib/Pwm')
+const { RTCPeerConnection, MediaStream } = require("wrtc")
+const HOST = 'ws://10.10.3.196:7896'
+class CarServer {
+    constructor(HOST) {
+        this.webRTC = null
+        this.mediaStream = null
+        this.videoSource = null
+        this.audioSource = null        
+        this.socket = io(HOST, {
+            auth: {
+                roomID: "feiCar",
+                name: 'car'
+            }
+        });
+        this.socket.on('connect', () => {
+            this.connect()
+        })
+        this.socket.on('leaved', user => {
+            console.log(`${user.name}离开${user.roomID}房间`)
+            // 用户离开移除RTC
+            this.destroyed()
+        })
+        this.socket.on('joined', user => {
+            console.log(`${user.name}加入${user.roomID}房间`)
+            // 发送offer
+            this.createOffer()
+        })
+        this.socket.on('msg', data => {
+            console.log('用户信息', data.type);
+            this.msg(data)
+        })
+        this.socket.on('connect_error', err => {
+            console.log('连接错误', err)
+            this.destroyed()
+        })
+    }
+    // 连接
+    connect() {
+        console.log('连接成功,开始初始化webrtc')
+        // 初始化rtc
+        this.webRTC = new RTCPeerConnection()
+        // 初始化媒体源
+        this.mediaStream = new MediaStream()
+        this.videoSource = new RTCVideoSource()
+        this.audioSource = new RTCAudioSource()
+        // 加入媒体源
+        const videoTrack = this.videoSource.createTrack()
+        this.mediaStream.addTrack(videoTrack)
+        this.webRTC.addTrack(videoTrack, this.mediaStream)
+        // 加入音频源
+        const audioTrack = this.audioSource.createTrack()
+        this.mediaStream.addTrack(audioTrack)
+        this.webRTC.addTrack(audioTrack, this.mediaStream)
+        // 加入媒体
+        this.videoSource.start()
+        this.audioSource.start()
+        // 监听ice
+        this.webRTC.onicecandidate = event => {
+            if (event.candidate) {
+                // 发送ICE
+                this.socket.emit('msg', {
+                    type: 'candidate',
+                    candidate: event.candidate
+                })
+            }
+        }
+        // 监听ice状态
+        this.webRTC.oniceconnectionstatechange = () => {
+            if (this.webRTC.iceConnectionState === 'failed' || this.webRTC.iceConnectionState === 'closed' || this.webRTC.iceConnectionState === "disconnected") {
+                console.log('ICE 连接失败')
+                this.destroyed()
+            }
+        }        
+    }
+    // 用户信息
+    msg(data){
+        if (data.type === 'answer') {
+            // 设置本地应答
+            this.webRTC.setRemoteDescription(data.answer)
+        } else if (data.type === 'candidate') {
+            // 本地设置ICE
+            this.webRTC.addIceCandidate(data.candidate)
+        } else if (data.type === 'conctrl') {
+            // 控制信息
+            //PWM.changPWM(data.conctrl)
+        } else if (data.type === 'startRTC') {
+            // 向其它房间人发送offer
+            this.createOffer()
+        }
+    }
+    // 创建offer
+    async createOffer() {
+        // 创建offer
+        const offer = await this.webRTC.createOffer()
+        this.webRTC.setLocalDescription(offer)
+        // 发送offer
+        this.socket.emit('msg', {
+            type: 'offer',
+            offer: offer
+        })
+    }
+
+    destroyed() {
+        if (this.webRTC) this.webRTC.close()
+        if (this.audioSource) this.audioSource.stop()
+        if (this.videoSource) this.videoSource.stop()
+        this.webRTC = null
+        this.mediaStream = null
+        this.videoSource = null
+        this.audioSource = null       
+        process.exit(1) 
+    }    
+}
+new CarServer(HOST)

+ 147 - 0
lib/Pwm.js

@@ -0,0 +1,147 @@
+const i2cBus = require("i2c-bus")
+const { Pca9685Driver } = require("pca9685")
+const RPIO = require("rpio");
+
+class ContrlService {
+    constructor() {
+        // PWM 和 舵机配置
+        this.pwmOption = {
+            i2c: i2cBus.openSync(1), // 树莓派1:scl1
+            address: 0x40, //板子地址
+            frequency: 50, //频率
+            debug: false
+        }
+        //PCA 板子位置电调+舵机
+        this.MoAndSero = {
+            m1: 0,
+            m2: 1,
+            s1: 2,
+            s2: 3
+        }
+        // prio init
+        RPIO.init({ mapping: "gpio" });
+        // 继电器解锁
+        RPIO.open(22, RPIO.OUTPUT, RPIO.LOW);
+        RPIO.open(23, RPIO.OUTPUT, RPIO.LOW);
+        // 开灯
+        RPIO.open(22, RPIO.OUTPUT, RPIO.HIGH);
+        // init PWM
+        this.PWM = new Pca9685Driver(this.pwmOption, er => {
+            if (er) {
+                console.error("Error initializing PCA9685");
+            }
+        })
+        // 解锁计数器
+        this.Snum = 0
+        // 解锁状态
+        this.enable = false
+    }
+    // 设置pwm
+    async changPWM (params) {
+        const { v0, v1, v2, v3 } = params
+        if (typeof (v0) == 'number' || typeof (v3) == 'number') {
+            if (this.enable) {
+                // 解锁之后需要2秒给电调中立值
+                if (this.Snum >= 50 && this.Snum <= 61) this.Snum++
+                if (this.Snum >= 60) {
+                   if (Math.abs(+v2 - 128) < 20) {
+                        console.log('前后', v3)
+                        this.PWM.setPulseLength(this.MoAndSero.m1, this.Motor2pwm(Math.abs(+v3 - 256)));
+                        this.PWM.setPulseLength(this.MoAndSero.m2, this.Motor2pwm(Math.abs(+v3 - 256)));
+                    } else {
+                        // 分左右
+                        if ((+v2 - 128) > 20) {
+                            // 右
+                            console.log('右', +v2);
+                            const a = 128 - (+v2 - 128)
+                            const b = 128 + (+v2 - 128)
+                            this.PWM.setPulseLength(this.MoAndSero.m1, this.Motor2pwm(+a));
+                            this.PWM.setPulseLength(this.MoAndSero.m2, this.Motor2pwm(+b));
+                        } else if (+v2 - 128 < -20) {
+                            // 左
+                            console.log('左', +v2);
+                            const a = 128 - (+v2 - 128)
+                            const b = 128 + (+v2 - 128)
+                            this.PWM.setPulseLength(this.MoAndSero.m1, this.Motor2pwm(+a));
+                            this.PWM.setPulseLength(this.MoAndSero.m2, this.Motor2pwm(+b));
+                        }
+                    }
+                    //云台
+                    if (v0 || v1) {
+                        console.log('云台', v0, v1)
+                        const a = +v1 > 200 ? 200 : +v1
+                        this.PWM.setPulseLength(this.MoAndSero.s1, this.Servo2pwm(+v0));
+                        this.PWM.setPulseLength(this.MoAndSero.s2, this.Servo2pwm(+a));
+                    }
+                }
+            } else {
+                this.unLOCK(v0, v1, v2, v3)
+            }
+        } else {
+            console.log('参数格式错误,自动略过');
+            return
+        }
+    }
+
+    /**
+     * 内八解锁
+     * @param {*} v0 
+     * @param {*} v1 
+     * @param {*} v2 
+     * @param {*} v3 
+     */
+    unLOCK (v0, v1, v2, v3) {
+        // 未解锁=>进行解锁:发送端的频率是0.04S,0.05*60 = 3S        
+        // 内八 :v0<50,v1>200,v2>200,v3<50
+        if (+v0 <= 60 && +v1 >= 190 && +v3 >= 190 && +v2 <= 60) {
+            if (this.Snum >= 0 && this.Snum <= 51) this.Snum++
+        } else {
+            this.Snum = 0
+        }
+        if (this.Snum == 50) {
+            RPIO.open(23, RPIO.OUTPUT, RPIO.HIGH);
+            this.enable = true
+        }
+        console.log('解锁中', v0, v1, v2, v3)
+    }
+    /**
+     * 电调换算比例:摇杆范围0-256,PWM范围500~1500~2500
+     * @param {number} v 
+     * @returns 
+     */
+    Motor2pwm (v) {
+        // 分为6挡位,中值128,步进256/6
+        const num = parseInt(256 / 6)
+        // 方向调换
+        const dirc = Math.abs(v - 256)
+        // 真实值
+        let value = 128
+        // 步进取中间值
+        if (dirc > 0 && dirc < num) {
+            value = num / 2
+        } else if (dirc > num && dirc <= num * 2) {
+            value = num + (num / 2)
+        } else if (dirc > num * 2 && dirc < 128) {
+            value = 128 - (num / 2)
+        } else if (dirc > 128 && dirc <= (128 + num)) {
+            value = 128 + (num / 2)
+        } else if (dirc > (128 + num) && dirc <= (128 + num * 2)) {
+            value = 128 + num + (num / 2)
+        } else if (dirc > (128 + num * 2) && dirc <= 256) {
+            value = 256 - (num / 2)
+        } else {
+            value = 128
+        }
+
+        return parseInt(value / (256 / 2000)) + 500
+    }
+    /**
+     * 舵机换算比例:摇杆范围0-256,PWM范围1000~1500~2000
+     * @param {number} v 
+     * @returns 
+     */
+    Servo2pwm (v) {
+        return parseInt(v / (256 / 1000)) + 1000
+    }
+}
+module.exports = new ContrlService()

+ 71 - 0
lib/audioSource.js

@@ -0,0 +1,71 @@
+const { RTCAudioSource } = require('wrtc').nonstandard;
+const ffmpeg = require('fluent-ffmpeg')
+
+class AudioSourceService extends RTCAudioSource {
+    constructor() {
+        super();
+        this.command = null // 存储音频源
+        this.cache = Buffer.alloc(0)
+    }
+
+    // 创建通道
+    createTrack() {
+        const track = super.createTrack()
+        return track
+    }
+
+
+    // 获取音频源
+    start() {
+        if (this.command !== null) this.stop()
+        this.command = ffmpeg("plughw:1,0")
+            .inputFormat("alsa")
+            .audioChannels(1)
+            .audioFrequency(48000)
+            .audioCodec("pcm_s16le")
+            .outputFormat("s16le")
+            .on("start", () => {
+                console.log("Audio start !");
+            })
+            .on("error", function (err) {
+                console.log('Audio processing an error occurred: ' + err.message);
+                // 退出进程
+                process.exit(1)
+            })
+        this.ffstream = this.command.pipe();
+        this.ffstream.on("data", (buffer) => {
+            // console.log("Audio buffer length", buffer.length);
+            this.cache = Buffer.concat([this.cache, buffer]);
+        });
+
+        const processData = () => {
+            while (this.cache.length > 960) {
+                const buffer = this.cache.slice(0, 960);
+                this.cache = this.cache.slice(960);
+                const samples = new Int16Array(new Uint8Array(buffer).buffer);
+                this.onData({
+                    bitsPerSample: 16,
+                    sampleRate: 48000,
+                    channelCount: 1,
+                    numberOfFrames: samples.length,
+                    type: "data",
+                    samples,
+                });
+            }
+            if (this.command !== null) {
+                setTimeout(() => processData(), 10);
+            }
+        };
+        processData();
+    }
+
+    // 停止获取
+    stop() {
+        console.log("audio source stop");
+        if (this.command != null) {
+            this.command.kill("SIGHUP")
+            this.command = null
+        }
+    }
+}
+module.exports = AudioSourceService

+ 68 - 0
lib/videoSource.js

@@ -0,0 +1,68 @@
+const { RTCVideoSource } = require('wrtc').nonstandard;
+const ffmpeg = require('fluent-ffmpeg')
+
+class VideoSourceService extends RTCVideoSource {
+    constructor() {
+        super() //videosource int
+        this.command = null // 存储视频源
+        this.cache = Buffer.alloc(0)
+    }
+
+    // 创建通道
+    createTrack() {
+        const track = super.createTrack()
+        return track
+    }
+
+
+    // 获取视频源
+    async start() {
+        if (this.command !== null) this.stop()
+        const width = 400, height = 300
+        // libx264编码后花屏
+        this.command = ffmpeg('/dev/video0')
+            .size(`${width}x${height}`)
+            .outputOptions(['-pix_fmt yuv420p'])
+            .format("rawvideo")
+            .on('start', () => {
+                console.log('Video start !');
+            })
+            .on('error', (err) => {
+                console.log('Video processing An error occurred: ' + err.message);
+                // 退出进程
+                process.exit(1)
+            })
+        this.ffstream = this.command.pipe();
+        this.ffstream.on('data', (buffer) => {
+            this.cache = Buffer.concat([this.cache, buffer])
+        });
+
+        // 整理buffer
+        const frameSize = width * height * 1.5;
+        const processData = () => {
+            while (this.cache.length > frameSize) {
+                const buffer = this.cache.slice(0, frameSize)
+                this.cache = this.cache.slice(frameSize)
+                this.onFrame({
+                    width,
+                    height,
+                    data: new Uint8ClampedArray(buffer)
+                })
+            }
+            if (this.command !== null) {
+                setTimeout(() => processData())
+            }
+        }
+        processData()
+    }
+
+    // 停止获取
+    stop() {
+        console.log("Video source stop");
+        if (this.command != null) {
+            this.command.kill("SIGHUP")
+            this.command = null
+        }
+    }
+}
+module.exports = VideoSourceService

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "car",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "socket.io-client": "^4.4.1",
+    "i2c-bus": "^5.2.2",
+    "pca9685": "^5.0.0",
+    "rpio": "^2.4.2",
+    "wrtc": "^0.4.7",
+    "fluent-ffmpeg": "^2.1.2"
+  }
+}