ESP32Cam人工智能教学20

ESP32Cam人工智能教学20

ESP32Cam专用APP

这次我们专门为ESP32Cam量身定制一个手机APP。手机APP是客户端,利用Socket连接ESP32Cam,ESP32Cam成了服务器,实现Socket全双工的数据传输模式,还可以一边显示摄像头图像,一边传送自定义的数据,功能非常齐全。

这一课涉及的内容非常多,涉及的知识非常广,又非常的难。为了避免吓跑了大家,我准备从后面倒着往前讲,由易到难逐层推进。

  • 使用效果展示

我们按照上节课的内容(第十九课 UDP Socket服务器),把ESP32Cam设计成一个视频及数据收发的服务器,通电后开启192.168.1.180:8080端口服务。手机安装了专用的APP,连接ESP32Cam的服务端口,成为客户端。(这时候手机和ESP32Cam在同一个网络中,也就是内网中。当然如果你利用星空隧道或者花生壳进行内网穿透FRP,可以把这个ESP32Cam的服务器端口推到公网中,实现用户的异地查看与操控)。

手机APP界面上面有一个连接按钮和两个文本框,用于连接到ESP32Cam,或者断开连接。一个图片控件,显示摄像头的图片。一个文本框和发送按钮,用于发送文本消息到ESP32Cam。一个文本显示标签控件,用于显示从ESP32Cam发送过来的消息。

我们对这个程序进行了一些优化。当ESP32Cam通电开机后,不断地检查客户端是否连接,堵塞并等待客户端的连接,也就是执行在主程序Loop的循环中。当检测到手机APP已连接在线的时候,会进入一个新的循环while (wclient.connected())当中,这时候,只要手机APP在线,就会一直执行这个收发模式的循环。在这个工作状态的循环体中,如果接收到了来自APP的数据(字符串hello),那么就把接收到的字符串前面加上“MSG”的标志,然后返回给手机APP(返回MSGhello)。同时每隔一定的延时,就发送一张摄像头的图片到手机APP中。

当然,如果我们按动手机APP上面的断开按钮,ESP32Cam会退出工作状态的while (wclient.connected())循环,串口显示“client out”,并重新返回到检测客户端连接的主循环Loop中,等待客户端的下一次连接成功。有了这样的机制后,我们可以随时断开手机APP的连接、或者重连,都不会造成程序崩溃。(当然这个ESP32Cam只能有一个客户端连接,因为这个开发板还是比较弱的,所以我们就设置了只允许一个客户端连接。如果需要允许多个客户端同时连接的话,就需要使用线程了,开发出一个主线程,然后给每个连接的客户端都开发出一个新的独立的工作线程。)

#include <WiFi.h>
#include <WiFiClient.h> 
#include <WiFiServer.h>
#include "esp_camera.h"

// 摄像头引脚 CAMERA_MODEL_WROVER_KIT
#define PWDN_GPIO_NUM    -1
#define RESET_GPIO_NUM   -1
#define XCLK_GPIO_NUM    21
#define SIOD_GPIO_NUM    26
#define SIOC_GPIO_NUM    27

#define Y9_GPIO_NUM      35
#define Y8_GPIO_NUM      34
#define Y7_GPIO_NUM      39
#define Y6_GPIO_NUM      36
#define Y5_GPIO_NUM      19
#define Y4_GPIO_NUM      18
#define Y3_GPIO_NUM       5
#define Y2_GPIO_NUM       4
#define VSYNC_GPIO_NUM   25
#define HREF_GPIO_NUM    23
#define PCLK_GPIO_NUM    22

const char* ssid = "ChinaNet-xxVP";
const char* password = "123456789";

#define CLIENTS_MAX_NUMS  1
WiFiServer server(8080);
WiFiClient serverClients[CLIENTS_MAX_NUMS];

int ac=0;

void setup() {
    Serial.begin(115200);//开启串口

    camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  //config.frame_size = FRAMESIZE_QVGA;   //320 * 240
  config.frame_size = FRAMESIZE_HQVGA;   //240 * 176
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 10;
  config.fb_count = 1;

  if (psramFound()) {
    config.jpeg_quality = 10;
    config.fb_count = 2;
    config.grab_mode = CAMERA_GRAB_LATEST;

  } else {
    // Limit the frame size when PSRAM is not available
    config.frame_size = FRAMESIZE_HQVGA;
    config.fb_location = CAMERA_FB_IN_DRAM;
  }


  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  WiFi.begin(ssid, password);
  WiFi.setSleep(false);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println(WiFi.localIP());
  delay(500);
  Serial.println("Start tcp server...");
  server.begin();
  server.setNoDelay(true);

}

void loop() {
    if (server.hasClient())
    {
        if (!serverClients[0] || !serverClients[0].connected())
        {
            if (serverClients[0])
            {
                serverClients[0].stop();
            }
            serverClients[0] = server.available();
            ac=0;
        }
    }
    if (serverClients[0] && serverClients[0].connected())
    {  //程序堵塞,等待客户端连接
        WiFiClient wclient = serverClients[0];
        camera_fb_t *fb = NULL;
        size_t _jpg_buf_len = 0;
        uint8_t *_jpg_buf = NULL;
        int bs, bss;
        Serial.println("clinet in");
        while (wclient.connected())
        {
            if (wclient.available())
            {  //当接收到来自客户端的数据时
                while (wclient.available()) 
                {
                    String recv_data = wclient.readStringUntil('\r');
                    Serial.println(recv_data);
                    recv_data = "MSG" + recv_data;
                    wclient.println(recv_data);
                }
            }
            
            fb = esp_camera_fb_get();
            if (fb)
            {  // 从摄像头获取图片的数据
                _jpg_buf_len = fb->len;
                _jpg_buf = fb->buf;
                bs = _jpg_buf_len / 1024;
                bss = _jpg_buf_len % 1024;
                //分成几个数据包进行发送
                for(int j=0; j<bs; j++)
                {
                    wclient.write(_jpg_buf, 1024);
                    for(int i=0; i<1024; i++)
                    {
                        _jpg_buf++;
                    }
                    delay(40); 
                }
                wclient.write(_jpg_buf, bss);
               
                Serial.println(_jpg_buf_len);
            }

            if (fb)
            {
                esp_camera_fb_return(fb);
                fb = NULL;
                _jpg_buf = NULL;
            }
            else if (_jpg_buf)
            {
                free(_jpg_buf);
                _jpg_buf = NULL;
            }
            //这里做个延时,根据你的网络速度调整,在调试时可以延时三四秒发一张
            delay(5000); 
        }
        Serial.println("clinet out");

    }
    delay(1000);
}

  • APPInventor制作手机APP

APPInventor是一款适合小学生学习的手机APP制作软件,采用可视化的积木块程序编辑模式,可以在线的方式制作自己的手机APP。网上提供APPInventor服务的服务器有华南理工、广州服务器、上海服务器等,现在好像只剩下广州服务器是免费的,其他两个都已经VIP收费使用了。

我们访问APPInventor广州服务器官网,用QQ账号注册,登录后就可以免费使用了。

这个程序需要两个自定义的组件支持,其中一个网络连接的组件是我自己编写的,另外一个BASE64编码的组件,是我花了40大洋从上海服务器购买的。为了方便大家,我把这个程序的源代码打包到百度网盘,大家可以去下载

链接:https://pan.baidu.com/s/1_PsfLAwq-NRnC5LCCkBJMw?pwd=k4h9

下载到电脑中的是一个aia后缀名的文件。接下来我们可以登录APPInventor广州服务器,点击顶端的“项目——导入项目”菜单,把这个aia文件上传,就能看到这个APP的源代码了。

手机APP的设计分为前端和后台两个部分,组件设计是前端,是和用户之间面对面的。可以看到,我们把一些文本输入框、按钮控件、图片控件、文字显示控件等,按照一定的顺序在手机屏幕中进行排列。这样就给用户提供了操作的方便,这个前端也就是手机中打开APP看到的样子了。

如果说前端的组件设计提供了漂亮的皮囊,那么后台的程序就是提供了有趣的灵魂。点击窗口右上角的“逻辑设计”可以切换到后台的程序设计界面。在这里我们可以看到按钮1是“连接 / 断开”按钮,通过自定义组件SocketClient1,进行网络连接或端口。右边的三个是当APP接收到来自摄像头的图像时,经过自定义组件进行BASE64解码,并发送到图片框进行显示。当接收到文本信息时(文本信息以MSG这三个字符开头为标志),则显示在文本标签中。当按动按钮2发送消息的按钮时,则调用自定义组件的发送消息程序块。

这时候,我们可以点击顶端的“打包APK——显示二维码”菜单,程序会进行编译,编译结束后,会显示一个二维码。我们用手机中的微信扫一扫,然后用浏览器打开,就能下载到一个APK安装文件,接着会自动安装,安装完成后,我们就可以在手机的桌面上看到这个ESP32Cam的专用APP了。打开这个APP,就能进行前面第一步的ESP32Cam的连接测试了。

我们可以看到,有了APPInventor,制作一个手机APP都是积木化的编程操作了。所以说小学生也能制作自己的手机APP,这下你应该相信了吧。不过我们从这个程序设计界面可以看到,所有的复杂的程序代码,都已经并编译成自定义组件,编程一个个可以拖动拼接的程序积木块了。

  • 打包自定义组件

有些人可能会好奇,究竟这样的程序块,后面隐藏着怎样复杂的程序代码呢?我们要怎样才能制作自己的自定义组件呢。前面程序中,负责网络连接的那个sockclient自定义组件就是由我自己编写代码,并打包完成的。

接下来,我记录一下自己是怎样制作和打包自定义模块的学习过程吧,怕有一天要用了,忘得一干二净了呢。这里很多都参考了下面博主“作业乃身外之物”的这篇博客

App Inventor插件开发(一)配置与测试_app inventor 插件 树树-CSDN博客

(一).  下载、安装、配置环境变量

想要制作和打包自己的自定义模块,需要在电脑中至少安装四个东西:

GIT   我下的是2.26.2

JDK   版本不要超过8,所以我下载的是7u79

ANT  我下的是1.9.14

appinventor   这个是好像是inventor的离线环境吧

note  我是用这个编辑插件的java代码的。

我的电脑是64位win7的(我怕这些软件版本低了,在高版本的Windows会不兼容出错),我会把相关的软件放在百度网盘中,需要的也可以自行去下载。

链接:https://pan.baidu.com/s/1pY9NsuAbPzORsm8Tm7knEQ?pwd=7tp9

https://p.ananas.chaoxing.com/star3/origin/7d19d0404bb69ff361a9b91e86bdc4c6.jpg

    

把GIT安装在C盘默认目录中

JDK安装在C盘默认目录(我怕装在其他盘配置会出问题),装完后,要配置系统环境变量:

新建JAVA_HOME为安装目录如

C:\Program Files\Java\jdk1.7.0_79

新建CLASSPATH为  

      .;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar

Path末尾添加(如果原来的末尾没有分号,要补上)

      %JAVA_HOME%/bin;%JAVA_HOME%/jre/bin;

检验JAVA安装是否正确:cmd输入java -version,输出版本信息即为成功。

https://p.ananas.chaoxing.com/star3/origin/75bee947afed9ce8b88a5f17df03aa16.jpg

Ant环境变量配置

新建ANT_HOME  为安装(解压)位置如

      C:\Program Files\ant\apache-ant-1.9.14

CLASSPATH末尾添加   ;%ANT_HOME%\lib;

Path中添加   ;%ANT_HOME%\bin;C:\Program Files\Git\cmd

检验:cmd输入ant,输出如下则为配置成功

https://p.ananas.chaoxing.com/star3/origin/ef7ea04fee6c71e980fcdb62eeb17937.jpg

把appinventor解压到D盘。

(二).  打包 aix  插件

我提供的安装程序中,有一个测试插件,把里面的cn文件夹解压到appinventor的安装文件夹下面的指定文件夹中。

解压出来的就是两个java后缀名的文件,可以用记事本打开查看。

大家千万要把文件的路径做对了,否则出错是没商量的。我们可以看到,这两个文件夹的路径中,src文件夹下面的路径,和这个 java文件的第一行中的路径是一致的。

https://p.ananas.chaoxing.com/star3/origin/c036c286fa331fbedb92270566349db6.jpg

我们在appinventor文件夹中,按住键盘的shift,用鼠标在空白的地方右击,选“在此处打开命令窗口”

然后在打开的cmd命令窗口中,输入ant extensions,按回车。

https://p.ananas.chaoxing.com/star3/origin/b9573a167f71f3b1016d26e0d4a74162.jpg

程序就开始执行打包了,如果出现后面的画面,就说明打包成功了。我们可以在里面的显示的路径下面,找到了已经打包好的 aix 的文件了。

https://p.ananas.chaoxing.com/star3/origin/32d66eb7d309f878a0aef9c643bdc025.jpg

这样,就有了自己的自定义组件了,也就可以把他导入到网上的APP项目中进行使用了。我们在组件前端的编辑界面,选择窗口左侧的积木仓库的底部,选择导入自定义组件按钮,然后选择刚才打包得到的aix文件,这样这个socketclient自定义组件就会显示在仓库的底部了。我么可以把他们拖入到APP的界面中了进行使用了。

四.  编辑 java  程序代码

刚才展示的仅仅是自定义组件的打包过程而已,是已经提供了程序的代码,在此基础上完成打包的动作。

当然,你如果你会 java代码,可以用note这个小程序打开文件进行程序代码的编写了。这样,我们就可以编写专属于自己的自定义模块了。这就是开源设计的优势,要怎样的功能插件都可以自己编写、打包、上传、分享、使用。

https://p.ananas.chaoxing.com/star3/origin/a7abee0349f9211d60827c1fe2115414.jpg


    百度网盘中下载到的是我两年前的JAVA程序代码,主要是对博主“作业乃身外之物”的原来的插件进行修改而成的。他原来的客户端插件中,客户端只有发送,没有从服务端接收的功能块。所以这次我着重修改、增加了客户端的接收部分。

这里有许多的知识点稍微提一下:

(1)在完成Socket连接后,创建一个线程,用于无限循环地监听,是否接收到来自服务器端的(ESP32Cam)的消息。

(2)如果接收到了新的消息(字符串或二进制数据),则转换成字符串,然后利用Message消息的方式,传递给Handle,用于修改UI的内容。这个是JAVA的运行机制,UI界面的一些属性修改,是放在主线程里进行的;而接收监听的子线程,是无法直修改UI的内容,需要把字符串传递给Handle进行修改。

(3)我们对接收到来自ESP32Cam的数据进行识别。接收到的数据无非只有两种,要么是字符串,要么是JPG图片。所以我们在这里做了个界定,ESP32Cam发送过来的字符串,都必须以“MSG”这三个字母为开头作为标志,APP接收到一组数据后,先检测是否MSG开头,如果是则由getMessage函数把字符发送到文本标签中显示。

JPG图片的数据比较大,所以我们采用分段发送(前一课的内容),每次发送1024个字节。JPG图片数据都是以0xFF  0xD8为开头,以0xFF  0xD9为结束。我们在接收数据的时候,会一组一组地接收,并把数据串接在一起。

(4)当图片数据结束时(检测到了结束标志),我们需要把图片的二进制数据byte数组,经过BASE64,转换成一个非常长的字符串,把这个字符串通过getJpg函数传递出来。

(5)经过我的这个自定义网络连接组件,可以负责APP与ESP32Cam之间的连接、数据传递收发。但是也存在一个问题,那就是我这个自定义组件接收到的摄像头图片,最后转化成了一个BASE64字符串。这个字符串是无法之间给图片控件显示的。

经过网上查找,我发现了上海的APPInventor服务器fun123.cn,可以提供BASE64字符串转换成图片的自定义组件。于是我就花了40大洋,注册了一个月的VIP用户,下载到了这个组件。

(7)其实网上也有人提供了一些其他的方法,就是当你接收到了图片数据后,把这些数据写入到手机的临时文件中(图片文件)。然后让图片控件去读取手机临时的图片文件并显示。这种方法是每收到一张图片,就要写一次的临时图片文件。这样频繁的存储操作是很低效的,也是我很反感的。

我虽然已经花钱得到了那个BASE64转图片的组件,但是我也看不到这个组件的源代码,我想这个组件应该不会使用存储临时文件这种拙劣的方法吧,否则怎么对得起我花的钱呢?

package cn.roger.socket;

import com.google.appinventor.components.annotations.*;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.*;
import com.google.appinventor.components.runtime.util.*;
import com.google.appinventor.components.runtime.errors.YailRuntimeError;
import android.graphics.drawable.GradientDrawable;
import android.graphics.Color;
import android.content.res.ColorStateList;
import android.view.View;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.Drawable;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.app.Activity;
import android.content.Context;
import android.view.Menu;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;

import java.util.Base64;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

@DesignerComponent(version = 1,
    description = "by Roger Young",
    category = ComponentCategory.EXTENSION,
    nonVisible = true,
    iconName = "images/extension.png")

@SimpleObject(external = true)

public class SocketClient extends AndroidNonvisibleComponent {
    Socket socket = null;
    OutputStream ou = null;
    String buffer = "";
    String geted1;
	Message msg;
    final int CONNECT = 100001;
    final int SENDMESSAGE = 100002;
    final int CLOSE = 100003;
    public SocketClient(ComponentContainer container) {
        super(container.$form());
    }
    public Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
			if(msg.what == 1){
				GetMessage(msg.obj.toString());
			}else if(msg.what == 2){
				GetJpg(msg.obj.toString());
			}
        }
 
    };
    @SimpleFunction(description = "start")
    public void closeConnect(){
        if(socket != null){
            try {
                ou.close();
                socket.close();
                socket = null;
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "关闭";
                myHandler.sendMessage(msg);
            }catch (IOException e) {
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "未知错误";
                myHandler.sendMessage(msg);
            }
        }else{
            GetMessage("连接未创建!");
        }
    }
    @SimpleFunction(description = "start")
    public void sendMessage(String s){
        if(socket != null){
            try {
                ou.write(s.getBytes("utf-8"));
                ou.write("\n".getBytes("utf-8"));
                ou.flush();
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "发送完毕";
                myHandler.sendMessage(msg);
            }catch (IOException e) {
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "未知错误";
                myHandler.sendMessage(msg);
            }
        }else{
            GetMessage("连接未创建!");
        }
    }
    @SimpleFunction(description = "start")
    public void connect(String ip, String port){
        if(socket == null){
			try {
				int po = Integer.valueOf(port);
                socket = new Socket();
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "开始连接";
                myHandler.sendMessage(msg);
                socket.connect(new InetSocketAddress(ip, po), 3000);
                ou = socket.getOutputStream();
				Thread mt = new Thread(){
					public void run() {
                        try {
							InputStream inst = null;
							byte[] buf = new byte[1060];
							byte[] buffer = new byte[30720];
							String msgstr = null;
							int len = 0;
							int pdd = 0;
							
							inst = socket.getInputStream();
                            while(true){
								if(!socket.isInputShutdown()) {
									buf = new byte[1060];
									len = inst.read(buf);
									
									if(buf[0] == 0x4D && buf[1] == 0x53 && buf[2] == 0x47){
										msgstr = null; 
										msgstr = new String(buf, 0, len);
                                        if(msgstr != null){
                                            msg = myHandler.obtainMessage();
											msg.what = 1; // 消息标识
                                            msg.obj = msgstr;
                                            myHandler.sendMessage(msg);
                                        }
									}else if((buf[0] & 0xFF)== 0xFF && (buf[1] & 0xFF) == 0xD8){
									    buffer = new byte[30720];
                                        for(int i=0; i<1024; i++){
                                            buffer[i]=buf[i];    
                                        }
                                		pdd = 1024;								
									}else if((buf[len-2] & 0xFF) == 0xFF && (buf[len-1] & 0xFF) == 0xD9){
                                        for(int i=0; i<len; i++){
                                            buffer[pdd + i]=buf[i];  										
                                        }
										pdd += len;
										//msgstr = String.format("jpg size  %d", pdd); 
                                        String jpgstr = Base64.getEncoder().encodeToString(buffer);
                                        msg = myHandler.obtainMessage();
										msg.what = 2; // 消息标识
                                        msg.obj = jpgstr;
                                        myHandler.sendMessage(msg);								
									}else if(len == 1024){
                                        for(int i=0; i<1024; i++){
                                            buffer[pdd + i]=buf[i];  										
                                        }
										pdd += 1024;
									}
                                } 
                            }
	                    } catch (IOException e) {
                            msg = myHandler.obtainMessage();
							msg.what = 1; // 消息标识
                            msg.obj = "他好像不见了";
                            myHandler.sendMessage(msg);
                            try{socket.close();}catch(Exception e1){}
	                    }
                    }
				};
				mt.start();
                msg = myHandler.obtainMessage();
                msg.obj = "连接成功";
                myHandler.sendMessage(msg);
            } catch (SocketTimeoutException aa) {
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "连接超时";
                myHandler.sendMessage(msg);
                socket = null;
            } catch (IOException e) {
                msg = myHandler.obtainMessage();
				msg.what = 1; // 消息标识
                msg.obj = "未知错误";
                myHandler.sendMessage(msg);
                socket = null;
            }
        }else{
            GetMessage("连接已创建!");
        }
    }

    @SimpleEvent
    public void GetMessage(String s){
        EventDispatcher.dispatchEvent(this, "GetMessage", "\n"+s);
    }

    @SimpleEvent
    public void GetJpg(String jpgstr){
        EventDispatcher.dispatchEvent(this, "GetJpg", "\n"+jpgstr);
    }
	
}

这样几百行的代码,看起来是否有密集恐惧症呢,这些没有一定的基础,确实是读起来很费劲,写起来就更难了,出了BUG调试那就难上加难了。程序猿就是这样一点一点地扣的。

为什么我会修改这段代码呢?因为我以前用eclipse做过手机APP,这些代码其实都是一样一样的。我这段时间也修改了eclipse做的用于连接开发板的手机客户端APP做了修改,也在之前的一些篇章中拿出来用过的。其实也没什么秘密武器,到最后都一点一点的拿出来了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tongyue

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值