0
智能家庭控制器这个项目忙活了将近一个多月,我的工作重点放在了图像传输和音频传输上,而控制方面我这没有做太多工作,因为之前做Orangepi Zero的时候已经做过了很多这些任务,参考以前的帖子,包括GPIO的控制、LCD5110的控制,I2C读取MPU6050等等,这里不再多做重复的工作,将重点放在核心上。
前面几个帖子已经介绍了使用UVC摄像头进行图像采集并进行jpeg图像转换的工作,后面我又做了一些音频方面的工作,在linux下进行音频采集和播放我使用的是基于oss框架的程序设计,alsa框架稍微麻烦一点。进行声音播放和录制其实很简单,和其他的设备使用方式一样就是打开、读写和关闭设备就行了。打开的设备是dsp设备,打开之后设置采集音频的参数包括采样率、量化位数、声道数。然后调用read函数就能进行进行录音了,获得的数据就是原始的PCM数据也就是原始音频数据,这些数据可以直接write到dsp设备上进行播放。下面就是使用dsp设备进行数据采集和播放的程序段:
- #define LENGTH 8 //录音时间,秒
- #define RATE 8000 //采样频率
- #define SIZE 8 //量化位数
- #define CHANNELS 1 //声道数目
- #define RSIZE 8192 //buf的大小
- static int recoder_fd = -1;
- int OpenRecoder()
- {
- recoder_fd = open("/dev/dsp", O_RDWR);
- if (recoder_fd < 0)
- {
- perror("Cannot open /dev/dsp device");
- return 1;
- }
- int arg = SIZE;
- if (ioctl(recoder_fd, SOUND_PCM_WRITE_BITS, &arg) == -1) //设置量化位数
- {
- perror("Cannot set SOUND_PCM_WRITE_BITS ");
- return 1;
- }
- arg = CHANNELS;
- if (ioctl(recoder_fd, SOUND_PCM_WRITE_CHANNELS, &arg) == -1) //设置声道数
- {
- perror("Cannot set SOUND_PCM_WRITE_CHANNELS");
- return 1;
- }
-
- arg = RATE;
- if (ioctl(recoder_fd, SOUND_PCM_WRITE_RATE, &arg) == -1) //设置采样率
- {
- perror("Cannot set SOUND_PCM_WRITE_WRITE");
- return 1;
- }
- }
- void CloseRecoder()
- {
- if(recoder_fd > 0)
- close(recoder_fd);
- }
- int Recording(unsigned char *buff,int len)
- {
- if(recoder_fd > 0)
- return read(recoder_fd, buff, len);
- else
- return 0;
- }
- int Playing(unsigned char *buff,int len)
- {
- if(recoder_fd > 0)
- write(recoder_fd, buff, len);
- }
复制代码
采集到的数据是不能直接保存成文件在Windows等系统识别,需要进行格式封装,这里进行的是wav格式的封装,wav的格式可以参考:http://blog.csdn.net/mlkiller/article/details/12567139,wav的数据格式表如下:
偏移地址
| 大小
字节
| 数据块
类型
| 内容
| 00H~03H
| 4
| 4字符
| 资源交换文件标志(RIFF)
| 04H~07H
| 4
| 长整数
| 从下个地址开始到文件尾的总字节数
| 08H~0BH
| 4
| 4字符
| WAV文件标志(WAVE)
| 0CH~0FH
| 4
| 4字符
| 波形格式标志(fmt ),最后一位空格。
| 10H~13H
| 4
| 整数
| 过滤字节(一般为00000010H)
| 14H~15H
| 2
| 整数
| 格式种类(值为1时,表示数据为线性PCM编码)
| 16H~17H
| 2
| 整数
| 通道数,单声道为1,双声道为2
| 18H~1BH
| 4
| 长整数
| 采样频率
| 1CH~1FH
| 4
| 长整数
| 波形数据传输速率(每秒平均字节数)
| 20H~21H
| 2
| 整数
| DATA数据块长度,字节。 | 22H~23H
| 2
| 整数
| PCM位宽
| 24H~27H | 4 | 4字符 | “fact”,该部分一下是可选部分,即可能有,可能没有,一般到WAV文件由某些软件转换而成时,包含这部分。
| 28H~2BH | 4 | 长整数
| size,数值为4 |
wav是我见过的最简单的文件格式,封装wav的函数也很简单:
- struct WAV_HEADER wavehead =
- {
- .RIFF = {'R','I','F','F'},
- .WAVE = {'W','A','V','E'},
- .fmt = {'f','m','t',' '},
- .appendinfo = 16,
- .format = 1,
- .desc = {'d','a','t','a'},
- };
- //保存采样率8000,采样深度8位,1声道的数据
- void SavePCM2WAV(unsigned char *buff,int len,char *file)
- {
- system("rm a.wav");
- system("touch a.wav");
-
- wavehead.lenth = len + sizeof(struct WAV_HEADER) - 8;
- wavehead.channel = 1;
- wavehead.freq = 8000;
- wavehead.rate = 8000*8/8;
- wavehead.sampling = 1;
- wavehead.datalenth = len;
- wavehead.depth = 8;
-
- int fd = open("a.wav",O_RDWR);
- if(fd > 0)
- {
- int ret = write(fd,(unsigned char*)(&wavehead),sizeof(struct WAV_HEADER));
- if(ret != sizeof(struct WAV_HEADER))
- {
- printf("write header errorrn");
- system("rm a.wav");
- return;
- }
- ret = write(fd,buff,len);
- if(ret != len)
- {
- printf("write data errorrn");
- system("rm a.wav");
- return;
- }
- close(fd);
- }
- }
复制代码
wav格式的文件虽然简单但是文件很大,不利于网络传输,这里可以使用命令将wav文件转成成mp3文件,文件大小会被压缩很多,使用的是lame库,可以自己安装或者编译lame源码:
lame sample.wav sample.mp3
得到的mp3文件就能进行网络传输了。
图像和音频基本上搞定了下面就是做主要的程序部分了,包括网络部分和硬件电路部分,由于NanoPi NEO2没有接出mic和扬声器我需要自己设计一个mic和扬声器放大的电路,我准备自己做一个PCB板,包含mic电路和扬声器放大电路,但是这里我犯了一个极为***的错误,我把可爱的NanoPi NEO2当成了orangepi zero了,我做板的时候都是按照orangepi zero的尺寸和电路做的,我真的是。。。看错电路拿错板子。不过我想程序都是Linux通用的,哪里跑都是一样的,我先用这个orangepi zero板子实验了就,因为pcb打板要很长的时间,我没时间等待了,现在就发出了结项贴了,实在抱歉。电路板的图片如下:
上面有mic和喇叭接口,喇叭是有功放的,声音很大。电路图如下:
功放电路
mic电路
USB扩展电路
等我的NanoPi NEO2的板子做好了得等一个多星期,到时候补一个帖子吧。主要还是创作的分享过程,硬件上大同小异,不过NanoPi NEO2的小巧身材的确加了很多分。我很喜欢,要是自带wifi的话就好了。
电路弄好了下面就是程序了。服务器使用的是公司的不开源的一个小型的服务器,并不是很牛的服务器,而是因为商业原因没有开源,所以只能使用不能研究了。控制器的核心网络部分的代码是一个状态机程序:
- void* network_thread(void *arg)
- {
- int i;
- printf("network_thread startrn");
- while(1)
- {
- if(network_stm == 0)
- {
- if(NetworkInit() == 0)
- {
- printf("Network Initialize successfullyrn");
- network_stm = 1;
- }
- else
- {
- printf("Network Initialize failedrn");
- usleep(5000000); //网络初始化失败的话延时一会再重连
- }
- }
- else if(network_stm == 1)
- {
- sprintf(login_string,"lock,%s,",key);
- printf("login_string:%srn",login_string);
- // write(sock_client,str,strlen(str));
- SendNetFrame(login_string,strlen(login_string));
- int len = read(sock_client,read_buff,1024);
- // printf("%drn",len);
- if(len > 0)
- {
- read_buff[len] = 0;
- printf("%srn",read_buff);
- if(strcmp(read_buff,"success") == 0)
- {
- printf("login successfullyrn");
- network_stm = 2;
- }
- else
- {
- printf("login failedrn");
- close(sock_client);
- network_stm = 0;
- usleep(5000000); //登录失败,关闭socket然后回到状态0
- }
- }
- else if(len == 0)
- {
- printf("connection closed by serverrn");
- network_stm = 0;
- }
- }
- else if(network_stm == 2)
- {
- int len = read(sock_client,read_buff,1024);
- if(len > 0)
- {
- // printf("read_buff len:%d,",len);
- // read_buff[len] = 0;
- // printf("%srn",read_buff);
- for(i=0;i
- {
- InputData(read_buff[i]);
- if(IsDataReady == 1)
- {
- IsDataReady = 0;
- input_frame[input_frame_len] = 0; //最后补0,防止进行字符串比较的时候数组越界
- printf("frame is ready,length:%drn",input_frame_len);
-
- if(input_frame[0] == 'p') //如果收到的是语言数据
- {
- printf("frame type:PCM datarn");
- OpenRecoder();
- Playing(input_frame+1,input_frame_len-1);
- CloseRecoder();
- }
- else if(input_frame[0] == 'c') //如果收到的是控制命令
- {
- printf("frame type:command:%srn",input_frame);
- if(strstr(input_frame,"get picture") != 0)
- {
- FLAG_GET_AND_SEND_PIC = 1;
- }
- else if(strstr(input_frame,"open lock") != 0)
- {
- FLAG_OPENDOOR = 1;
- }
- else if(strstr(input_frame,"stream_on_off") != 0)
- {
- FLAG_GET_VIDEO_STREAM = FLAG_GET_VIDEO_STREAM?0:1;
- }
- }
- }
- }
-
- }
- else if(len == 0)
- {
- printf("connection closed by serverrn");
- network_stm = 0;
- }
- }
- usleep(1000);
- }
- }
复制代码
用于解析接收到的网络数据,这是程序的核心,其他的摄像头和音频的部分前面都有程序不再说了。当收到来自 手机端发来的命令的时候网络线程就会处理接收到的命令判断是拍照命令还是来自手机上的音频数据等等。安卓手机上的程序我也是简单的做了一个,我不是专门做安卓的所以做的很粗糙,只是一个大致的结构:
还可以进行双边语言通信,通信方式和微信类似,按下说话松开发送,安卓这块的代码写的很垃圾,仅供参考:
- public class MainActivity extends AppCompatActivity {
- static Socket socket = null;
- OutputStream ou;
- InputStream in;
- LinearLayout main_linear_layout;
- ImageView image_view;
- ImageView big_image_view;
- Button btn_openlock;
- Button btn_getpic;
- Button btn_setup;
- Button btn_record;
- Handler handler = new Handler();
- byte send_buff[]; //发送数据缓冲区
- int send_buff_len; //发送数据缓冲区长度
- byte raw_data_buff[] = new byte[1024 * 1024]; //读取socket得到的原始数据
- byte frame_buff[] = new byte[1024*1024]; //原始数据解析得到的有效数据帧
- boolean IsFrameReady = false;
- int frame_buffLen;
- int state = 0;
- int count = 0;
- Bitmap bitmap;
- byte[] img_buf = new byte[1024 * 1024]; //接收到的jpeg图片数据
- byte[] mp3_buf = new byte[1024 * 1024]; //接收到的mp3数据
- private NotificationManager notificationManager;
- Thread RecordThread = null;
- AudioRecord audioRecord = null;
- byte[] RecordBuffer = new byte[8000 * 20];
- int RecordBufferLen = 0;
- int audioSource = MediaRecorder.AudioSource.MIC;
- //设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
- int sampleRateInHz = 8000;
- //设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
- int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
- //音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。
- int audioFormat = AudioFormat.ENCODING_PCM_8BIT;
- int bufferSizeInBytes;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- setContentView(R.layout.activity_main);
- main_linear_layout = (LinearLayout)findViewById(R.id.main_linear_layout);
- image_view = (ImageView)findViewById(R.id.image_view);
- image_view.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- big_image_view.setVisibility(View.VISIBLE);
- main_linear_layout.setVisibility(View.GONE);
- }
- });
- big_image_view = (ImageView)findViewById(R.id.big_image_view);
- big_image_view.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- big_image_view.setVisibility(View.GONE);
- main_linear_layout.setVisibility(View.VISIBLE);
- }
- });
- btn_openlock = (Button)findViewById(R.id.btn_openlock);
- btn_openlock.setOnClickListener(btn_openlock_OnClickListener);
- btn_setup = (Button)findViewById(R.id.btn_setup);
- btn_setup.setOnClickListener(btn_setup_OnClickListener);
- btn_getpic = (Button)findViewById(R.id.btn_getpic);
- btn_getpic.setOnClickListener(btn_getpic_OnClickListener);
- btn_record = (Button)findViewById(R.id.btn_record);
- // btn_record.setOnClickListener(btn_record_OnClickListener);
- btn_record.setOnTouchListener(btn_record_OnTouchListener);
- ReadThread.start();
- // 第一步:通过getSystemService()方法得到NotificationManager对象;
- notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- //动态申请权限,高版本的安卓系统需要动态申请权限才行,只修改manifest文件没用
- ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.RECORD_AUDIO},0);
- }
- //动态申请权限回调函数
- public void onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults) {
- switch (requestCode) {
- case 0: {
- // If request is cancelled, the result arrays are empty.
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Log.i("tangquan", "permission RECORD_AUDIO request success");
- } else {
- Log.i("tangquan", "permission RECORD_AUDIO request failed");
- }
- return;
- }
- }
- }
- //帧控制输入一个字节的数据
- void InputData(byte data)
- {
- if(state == 0)
- {
- if(IsFrameReady == false)
- {
- if(data == 'T')
- {
- state = 1;
- count = 0;
- }
- }
- }
- else if(state == 1)
- {
- if(data == '\')
- {
- state = 2;
- }
- else if(data == 'Q')
- {
- state = 0;
- IsFrameReady = true;
- frame_buffLen = count;
- }
- else
- {
- frame_buff[count++] = data;
- }
- }
- else if(state == 2)
- {
- state = 1;
- frame_buff[count++] = data;
- }
- }
- void SendFrame(char type,byte[] buff)
- {
- byte frame[] = new byte[buff.length * 2];
- int i,j = 0; //j用于计数frame的长度
- frame[j++] = 'T';
- frame[j++] = (byte)type;
- for(i=0;i
- {
- if(buff[i] == 'T' || buff[i] == 'Q' || buff[i] == '\')
- {
- frame[j] = '\'; //插入一个转义字符
- j++;
- }
- frame[j] = buff[i];
- j++;
- }
- frame[j] = 'Q';
- j++;
- SocketSendBytes(frame,j);
- }
- void SendFrame(char type,byte[] buff,int len)
- {
- byte frame[] = new byte[buff.length * 2];
- int i,j = 0; //j用于计数frame的长度
- frame[j++] = 'T';
- frame[j++] = (byte)type;
- for(i=0;i
- {
- if(buff[i] == 'T' || buff[i] == 'Q' || buff[i] == '\')
- {
- frame[j] = '\'; //插入一个转义字符
- j++;
- }
- frame[j] = buff[i];
- j++;
- }
- frame[j] = 'Q';
- j++;
- SocketSendBytes(frame,j);
- }
- Thread ReadThread = new Thread()
- {
- public void run()
- {
- socket = new Socket();
- try
- {
- // socket.connect(new InetSocketAddress("192.168.1.106",5000),1000);
- socket.connect(new InetSocketAddress("103.76.85.83",5000),1000);
- ou = socket.getOutputStream();
- in = socket.getInputStream();
- SocketSendStr("phone,AAAAEqo7V7dNvVP8,");//发送账号和密码
- int len = in.read(raw_data_buff,0,100);//read是阻塞式的,用SocketChannel可以不阻塞
- if(len > 0)
- {
- //需要进行截断,不然直接把result转成字符串那么这个字符串的长度是result的长度
- String res_str = new String(Arrays.copyOfRange(raw_data_buff, 0, len));
- if(res_str.equals("success"))
- {
- PostToast("连接成功");
- }
- else
- {
- PostToast("账号错误");
- socket.close();//关闭socket连接
- socket = null;
- return;
- }
- }
- }
- catch(Exception e)
- {
- socket = null;
- PostToast("连接失败");
- return;
- }
- while(socket != null)
- {
- try {
- int len = in.read(raw_data_buff,0,raw_data_buff.length);
- if(len > 0)
- {
- // PostToast("len:" + String.valueOf(len));
- for(int i=0;i
- {
- InputData(raw_data_buff[i]);
- //是否接受到一帧数据
- if(IsFrameReady)
- {
- IsFrameReady = false;
- // PostToast("get one frame,len:"+String.valueOf(frame_buffLen)+",type:"+String.valueOf((char)frame_buff[0]));
- Log.i("tangquan","get one frame,len:"+String.valueOf(frame_buffLen)+",type:"+String.valueOf((char)frame_buff[0]));
- if((char)frame_buff[0] == 'j')
- {
- // showNotification("tangquan","content","contenttext",0,0);
- for(int j=1;j
- {
- img_buf[j - 1] = frame_buff[j];
- }
- handler.post(new Runnable()
- {
- @Override
- public void run(){
- bitmap = BitmapFactory.decodeByteArray(img_buf,0,frame_buffLen-1);
- image_view.setImageBitmap(bitmap);
- big_image_view.setImageBitmap(bitmap);
- }
- });
- }
- else if((char)frame_buff[0] == 'm')
- {
- Log.i("tangquan", "get mp3 data");
- for(int j=1;j
- mp3_buf[j - 1] = frame_buff[j];
- String filename = "a.mp3";
- File file = new File(MainActivity.this.getFilesDir(),filename);
- FileOutputStream outputStream;
- try{
- outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
- outputStream.write(mp3_buf);
- outputStream.close();
- } catch(Exception e) {
- e.printStackTrace();
- }
- Log.i("tangquan", MainActivity.this.getFilesDir()+"/"+filename);
- MediaPlayer mp = new MediaPlayer();
- mp.setDataSource(MainActivity.this.getFilesDir()+"/"+filename);
- mp.prepare();
- mp.start();
- }
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- sleep(10);
- } catch (Exception ignored) {}
- }
- }
- };
- View.OnClickListener btn_openlock_OnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- PostToast("btn_openlock_OnClickListener");
- // showNotification("tangquan","content","contenttext",0,0);
- SendFrame('c',"open lock".getBytes());
- }
- };
- View.OnClickListener btn_getpic_OnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- SendFrame('c',"get picture".getBytes());
- }
- };
- View.OnClickListener btn_record_OnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Log.i("tangquan", "btn_setup press");
- }
- };
- boolean isRecording = false;
- View.OnTouchListener btn_record_OnTouchListener = new View.OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if(event.getAction() == MotionEvent.ACTION_DOWN)
- {
- Log.i("tangquan", "btn_record pressed");
- RecordThread = new Thread()
- {
- public void run()
- {
- // Create a new AudioRecord object to record the audio.
- // 获得满足条件的最小缓冲区大小
- bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
- Log.i("tangquan", "bufferSizeInBytes:" + String.valueOf(bufferSizeInBytes));
- // 创建AudioRecord对象
- audioRecord = new AudioRecord(audioSource, sampleRateInHz,channelConfig, audioFormat, bufferSizeInBytes);
- if(audioRecord != null)
- {
- audioRecord.startRecording();
- isRecording = true;
- RecordBufferLen = 0;
- while (isRecording) {
- RecordBufferLen += audioRecord.read(RecordBuffer, RecordBufferLen, bufferSizeInBytes);
- Log.i("tangquan", String.valueOf(RecordBufferLen));
- }
- audioRecord.stop();
- audioRecord.release();// 释放资源
- audioRecord = null;
- }
- else
- {
- Log.i("tangquan", "create AudioRecord failed");
- }
- }
- };
- RecordThread.start();
- }
- else if(event.getAction() == MotionEvent.ACTION_UP)
- {
- Log.i("tangquan", "btn_record released");
- if(RecordThread != null && audioRecord != null)
- {
- isRecording = false;
- new Thread()
- {
- public void run()
- {
- // AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat,bufferSizeInBytes, AudioTrack.MODE_STREAM);
- // audioTrack.play(); //放音
- // try {
- // audioTrack.write(RecordBuffer, 0, RecordBufferLen);
- // } catch (Exception e) {
- // Log.e("AudioTrack", "Playback Failed");
- // }
- SendFrame('p',RecordBuffer,RecordBufferLen);
- }
- }.start();
- }
- }
- return false;
- }
- };
- View.OnClickListener btn_setup_OnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- }
- };
- void SocketSendBytes(byte[] buff,int len) {
- send_buff = buff;
- send_buff_len = len;
- // Log.i("tangquan", String.valueOf(send_buff_len));
- new Thread()
- {
- @Override
- public void run() {
- if (socket == null)
- return;
- try {
- ou.write(send_buff,0,send_buff_len);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }.start();
- }
- void SocketSendStr(String str) {
- send_buff = str.getBytes();
- new Thread()
- {
- @Override
- public void run() {
- if (socket == null)
- return;
- try {
- ou.write(send_buff);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }.start();
- }
- //在线程中调用Toast会出错,不知道具体原因,但是网上说用 handler.post可以做
- //所以弄了一个类和一个函数,类是实现了Runnable接口,同时添加了一个字符串变量
- class MyRunnable implements Runnable
- {
- String Message;
- public MyRunnable(String msg)
- {
- Message = msg;
- }
- public void run(){
- }
- }
- void PostToast(String msg)
- {
- handler.post(new MyRunnable(msg)
- {
- @Override
- public void run(){
- Toast.makeText(MainActivity.this, Message, Toast.LENGTH_SHORT).show();
- }
- });
- }
- // 第二步:对Notification的一些属性进行设置比如:内容,图标,标题,相应notification的动作进行处理等等;
- public void showNotification(String tickerText, String contentTitle, String contentText, int iconId, int notiId) {
- Notification myNotify = new Notification();
- myNotify.icon = R.drawable.lock;
- myNotify.tickerText = "TickerText:您有新短消息,请注意查收!";
- myNotify.when = System.currentTimeMillis();
- // myNotify.flags = Notification.FLAG_NO_CLEAR;// 不能够自动清除
- // rv.setTextViewText(R.id.text_content, "hello wrold!");
- // myNotify.contentView = rv;
- Intent intent = new Intent(Intent.ACTION_MAIN);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 1,intent, FLAG_UPDATE_CURRENT);
- myNotify.contentIntent = contentIntent;
- notificationManager.notify(2, myNotify);
- }
- // 6步:使用notificationManager对象的cancelAll()方法取消
- public void clearNoti(View v) {
- notificationManager.cancelAll();// 清除所有
- }
- }
复制代码
至此家庭控制器已经做完了,可以进行家庭拍照和家庭语言对讲,唯一的不足时PCB做错了,这点真的很懊恼,PCB制版费还真不便宜,诶。这一个月的使用我对NanoPi有一些小感触,不得不说NanoPi的做工真的很好,说实话我不是可以夸大NanoPi贬低Orangepi,NanoPi的做工比Orangepi好很多,我看了一下板子的布线,NanoPi以如此小的面积布出这样的板子实在是厉害,真的很喜欢NanoPi NEO2,太小了太强大了,希望后面能够使用NEO2做更多有趣的东西,谢谢 电子发烧友提供的机会!
|
评分
-
查看全部评分
|