Bootstrap

OrangePi Zero2 全志H616 开发初探

一、刷机和系统启动

1、TF 卡格式化:

可以使用SD Card Formatter软件格式化TF

2、镜像烧录:

使用win32diskimager软件将准备好的镜像烧录到TF卡中

镜像下载网址🔗点击这里 点击Orange Pi Zero2官方镜像的ubuntu镜像,跳转至百度网盘,下载3.0.6版本:
在这里插入图片描述
此镜像可能默认关闭uart5,需要使用uart5时配置一下串口uart5,打开配置文件:

sudo vi /boot/orangepiEnv.txt

加入以下字段,开启uart5i2c3

overlays=uart5 i2c3

如图所示:

在这里插入图片描述

3、登录系统:

利用MobaXterm进行串口登录或SSH方式登录,初次登录应用串口登录或利用USBHTML接入屏幕后登录,默认用户如下:

  • 用户名:orangepi,密码:orangepi
  • 用户名:root,密码:orangepi

修改orangepi用户密码:sudo passwd orangepi

① 串口登录:

使用TTL 转 USB模块连接开发板串口(电脑需要安装ch340驱动),如图所示,然后接入电脑,使用MobaXterm软件进行串口登录,波特率默认为115200MobaXterm连接上串口后,插入开发板电源线,查看串口打印数据,检验刷机是否成功

在这里插入图片描述
② 重启及关机:

  • 重启:sudo reboot
  • 关机:sudo poweroff

③ 网络配置:

  • 扫描周围的WIFI热点:nmcli dev wifi
  • 接入网络:nmcli dev wifi connect user password xxxxxxxx    // 比如接入用户名为userwifi,密码为xxxxxxxx
  • 查看IP地址:ip addr show wlan0ifconfig

④ SSH 登录开发板:

镜像自带SSH服务器,只要通过MobaXterm登陆即可

二、基于官方外设开发

1、wiringPi 外设 SDK 安装:

  • git clone https://github.com/orangepi-xunlong/wiringOP -b master   // 下载源码
  • cd wiringOP        // 进入文件夹
  • sudo ./build clean    // 清除编译信息
  • sudo ./build        // 编译

    或者通过 windows 浏览器打开 https://github.com/orangepi-xunlong/wiringOP,下载压缩包,通过 MobaXterm 把压缩包传到开发板:
  • 解压:unzip xxx.zip
       cd xxx
       sudo ./build
  • 验证是否安装成功:gpio readall,如下图所示

在这里插入图片描述

上述1~26物理端口,以如图所示方向的引脚对应:

在这里插入图片描述

2、C 文件编译:

  • 使用wiringPi库,编译的时候需要链接:gcc xxx.c -o -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt
  • 可以制作简单的shell脚本便于编译:
    vi bulid.sh   // 编写 shell 文件
  • 加入以下代码:
    gcc $1 -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt   // $1 为参数
  • 保存文件后,为 shell 文件添加可执行权限:
    chmod +x build.sh
  • 运行时 wiringPi 需要访问底层驱动程序,使用超级用户权限运行:sudo ./a.out

3、基于官方外设的应用开发:

① GPIO 输入输出:
#include <stdio.h>
#include <wiringPi.h>
#include <unistd.h>

#define GPIO1 0
#define GPIO2 2

int main (void)
{
    wiringPiSetup();    // 初始化 wiringPi 库

    pinMode (GPIO1, OUTPUT);    	// 设置 IO 口为输出模式
    pinMode (GPIO2, INPUT);    		// 设置 IO 口为输入模式

    while(1){
        sleep(1);							// 延时 1 秒;
        									// usleep();	延时微秒
	    digitalWrite (GPIO1, HIGH);			// IO 口输出高电平
        sleep(1);
        digitalWrite (GPIO1, LOW);			// IO 口输出低电平

		if(digitalRead(GPIO2)){				// 检测 IO 口电平
			printf("high level\n");
		}else{
			printf("low level\n");
		}
		
    }

    return 0;
}
② 超声波测距:

时间函数:

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);

struct timeval{
		time_t      tv_sec;     /* seconds(秒)*/
		suseconds_t tv_usec;    /* microseconds(微秒)*/
};

struct timezone{
		int tz_minuteswest;     /* minutes west of Greenwich (格林威治时间往西方的时差) */
		int tz_dsttime;         /* type of DST correction (DST 时间的修正方式) */
};
  • 获取自1970-01-01 00:00:00到调用gettimeofday()函数所经历的秒数,存放在tv中,精确到微秒,在超声波测距应用中,我们只关心时间差值
  • 获取时区信息,存放到tz中,不关心时置NULL

例程:

#include <stdio.h>
#include <sys/time.h>
#include <wiringPi.h>
#include <stdlib.h>
#include <unistd.h>

#define Trig 5
#define Echo 7

double getDistance()
{
    double dis;
    struct timeval start;
    struct timeval end;

    pinMode(Trig, OUTPUT);
    pinMode(Echo, INPUT);
    digitalWrite(Trig ,LOW);
    usleep(5);

    digitalWrite(Trig ,HIGH);       /* 向 Trig 口发送 10 微秒 TTL 脉冲 */
    usleep(10);
    digitalWrite(Trig ,LOW);

    while(!digitalRead(Echo));      // 等待 Echo 口高电平
    gettimeofday(&start,NULL);      // 获取时间
    while(digitalRead(Echo));       // 等待 Echo 口低电平
    gettimeofday(&end,NULL);        // 获取时间

    long diffTime = 1000000*(end.tv_sec-start.tv_sec)+(end.tv_usec - start.tv_usec);    // 计算时间差值,单位:微秒
    dis = (double)diffTime/1000000 * 34000 / 2;         // 计算距离,音速为 340 m/s
    /*
        diffTime/1000000 转化为秒
        340(m/s) * 100 转化为厘米每秒(cm/s)
        往返为两段路程,需要 /2
    */
    return dis;
}

int main()
{
    double dis;

    if(wiringPiSetup() == -1){
        fprintf(stderr,"%s","initWringPi error");
        exit(-1);
    }

    while(1){
        dis = getDistance();
        printf("dis = %lf\n",dis);
        usleep(500000);
    }

    return 0;
}
③ Linux 定时器:
  • 实现定时器,通过itimerval结构体配置以及函数setitimer()产生的信号,系统随之使用signal信号处理函数来处理产生的定时信号,从而实现定时器
struct itimerval
{
	/* Value to put into `it_value' when the timer expires. */
	struct timeval it_interval;
	/* Time to the next timer expiration. */
	struct timeval it_value;
};

struct timeval
{
	__time_t tv_sec; /* Seconds. */
	__suseconds_t tv_usec; /* Microseconds. */
};
it_interval计时器的初始值,一般基于这个初始值加或减,基于控制函数的参数配置
it_value当程序运行到此,间隔多久启动定时器
tv_sec
tv_usec微秒(μs)(10-6s)
int setitimer (__itimer_which_t __which,
				const struct itimerval *__restrict __new,
				struct itimerval *__restrict __old);
返回值:成功返回 0,失败返回 -1
__which参数:ITIMER_REAL       // 数值为 0,计时器的值实时递减,发送的信号是 SIGALRM
ITIMER_VIRTUAL    // 数值为 1,进程执行时递减计时器的值,发送的信号是 SIGVTALRM
ITIMER_PROF       // 数值为 2,进程和系统执行时都递减计时器的值,发送的信号是 SIGPROF
__new参数:设定定时器相关设置
__old参数:保存先前__new的值,常设为NULL
  • 函数的第一个参数,我们使用ITIMER_REAL,那么显然,我们需要捕获对应的信号进行逻辑相关处理,捕获SIGALRM信号:signal(SIGALRM, signal_handler);
  • 该方法一个进程只能创建一个定时器

例程:

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

static int i;
void signal_handler(int signum)		// 每隔 2000 * 500 μs,即 1s 打印 "hello\n"
{
    i++;
    if(i == 2000){
        printf("hello\n");
        i = 0;
    }
}
int main()
{
    struct itimerval itv;

    // 设定定时时间
    itv.it_interval.tv_sec = 0;
    itv.it_interval.tv_usec = 500;		// 500 微秒
    
    // 定时时间设定完成后,间隔多久启动定时器
    itv.it_value.tv_sec = 1;
    itv.it_value.tv_usec = 0;
    // 间隔 1 秒启动定时器
    
    // 设定定时方式
    if( -1 == setitimer(ITIMER_REAL, &itv, NULL)){
		perror("error");
		exit(-1);
	}
    
    //信号处理
    signal(SIGALRM, signal_handler);
    
    while(1);
    return 0;
}
④ PWM 输出:
  • 输出高电平持续时间为1.0 ms,低电平持续时间为19 msPWM信号,即占空比为5%,周期为20 ms,频率为1 / 0.02s = 50 Hz

例程:

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>
#include <wiringPi.h>

#define PWM 0

static int i = 0;
void signal_handler(int signum)
{
    if(i <= 2){
        digitalWrite(PWM, HIGH);
    }else{
        digitalWrite(PWM, LOW);
    }
    if(i == 40){
        i = 0;
    }
    i++;
}

int main()
{
    struct itimerval itv;

    wiringPiSetup();
    pinMode(PWM, OUTPUT);

    itv.it_interval.tv_sec = 0;
    itv.it_interval.tv_usec = 500;		// 定时时间 500 微秒
    itv.it_value.tv_sec = 1;
    itv.it_value.tv_usec = 0;

    if( -1 == setitimer(ITIMER_REAL, &itv, NULL)){
        perror("error");
        exit(-1);
    }

    signal(SIGALRM, signal_handler);

    while(1);
    return 0;
}
⑤ OLED 显示屏应用——IIC协议:
  • Orange Pi Zero 226pin原理图可知,可用的i2ci2c3
  • 启动linux系统,确认/dev下存在i2c-3的设备节点,可以观察到系统支持I2C-3I2C-5的驱动,而H616的外设只有一个IIC接口,用的是IIC-3
  • 开始测试IIC,首先安装i2c-tools
                        sudo apt-get install i2c-tools
  • 接好线后,终端输入指令sudo i2cdetect -y 3,可以看到终端打印信息,如下图,表明已经通过i2c-3驱动扫描设备成功在这里插入图片描述

下面基于wiringPi库中的例程进行开发,路径:.../wiringOP-next/examples/oled_demo.c,建议阅读熟悉例程代码

#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>

#include "oled.h"
#include "font.h"

int oled_demo(struct display_info *disp) {
    int i;
    char buf[100];

    disp->font = font1;				// 设置字体类型
	// void oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str);
    oled_putstrto(disp, 0, 9+1, "hello world");		// 将字符信息写入缓存

    disp->font = font2;				// 设置字体类型,目测 font1 与 font2 区别不大
    oled_putstrto(disp, 0, 18+2, "hello world");        // 将字符信息写入缓存

    disp->font = font3;				// 设置字体类型,font3 是小字
    oled_putstrto(disp, 0, 27+3, "hello world");        // 将字符信息写入缓存


    oled_send_buffer(disp);				// 将缓存发送到物理屏幕上
    
	return 0;
}

void show_error(int err, int add) {
    //const gchar* errmsg;
    //errmsg = g_strerror(errno);
    printf("\nERROR: %i, %i\n\n", err, add);
    //printf("\nERROR\n");
}

void show_usage(char *progname) {
    printf("\nUsage:\n%s <I2C bus device node >\n", progname);
}

int main(int argc, char **argv) {
    char filename[32];
    struct display_info disp;

    if (argc < 2) {
        show_usage(argv[0]);

        return -1;
    }

    memset(&disp, 0, sizeof(disp));
    sprintf(filename, "%s", argv[1]);		// argv[1] 要传入需要使用的 IIC 驱动文件路径
    disp.address = OLED_I2C_ADDR;
    disp.font = font2;

    oled_open(&disp, filename);				// 与打开 IIC 驱动文件相关的函数
	oled_init(&disp);
	
	oled_demo(&disp);			// 显示内容处理函数
    
    return 0;
}
⑥ 串口开发:
  • 查看/dev可以看到串口驱动文件有:ttyS0ttyS1ttyS5ttyS0默认作为系统调试信息的串口,也可自行配置为通信串口使用。由Orange Pi Zero 226pin原理图可知,我们想要使用ttyS5串口

例程:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#include <stdlib.h>
#include <unistd.h>

int fd;
void* Sendhandler()				// 向串口发送数据
{
    char *sendBuf;
    sendBuf = (char *)malloc(32*sizeof(32));
    while(1){
        memset(sendBuf, '\0', 32);
        scanf("%s", sendBuf);
        while(*sendBuf){		// 检测 '\0'
            serialPutchar(fd, *sendBuf++);		// 向串口发送一个字符
        }
    }
}

void* Revhandler()				// 读取串口数据
{
    while(1){
        while (serialDataAvail(fd))
        {
            printf("%c", serialGetchar(fd));		// 读取串口一个字符并打印
            fflush(stdout) ;						// 清空输出缓冲区之类的操作
        }
    }
}

int main ()
{
    pthread_t idSend;
    pthread_t idRev;

    if ((fd = serialOpen ("/dev/ttyS5", 115200)) < 0)
    {
        fprintf (stderr, "Unable to open serial device: %s\n", strerror (errno)) ;
        return 1 ;
    }

	// 为了全双工地使用串口,我们分别创建了收和发的线程
    pthread_create(&idSend, NULL, Sendhandler, NULL);
    pthread_create(&idRev, NULL, Revhandler, NULL);

    if (wiringPiSetup () == -1)
    {
        fprintf (stdout, "Unable to start wiringPi: %s\n", strerror (errno)) ;
        return 1 ;
    }

    while(1){
        sleep(10);
    }

    return 0 ;
}
⑦ Linux 原生串口开发:

可以参考wiringPi库的串口源码wiringSerial.c,如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "wiringSerial.h"

/*
 * serialOpen:
 *	Open and initialise the serial port, setting all the right
 *	port parameters - or as many as are required - hopefully!
 *********************************************************************************
 */

int serialOpen (const char *device, const int baud)
{
  struct termios options ;
  speed_t myBaud ;
  int     status, fd ;

  switch (baud)
  {
    case      50:	myBaud =      B50 ; break ;
    case      75:	myBaud =      B75 ; break ;
    case     110:	myBaud =     B110 ; break ;
    case     134:	myBaud =     B134 ; break ;
    case     150:	myBaud =     B150 ; break ;
    case     200:	myBaud =     B200 ; break ;
    case     300:	myBaud =     B300 ; break ;
    case     600:	myBaud =     B600 ; break ;
    case    1200:	myBaud =    B1200 ; break ;
    case    1800:	myBaud =    B1800 ; break ;
    case    2400:	myBaud =    B2400 ; break ;
    case    4800:	myBaud =    B4800 ; break ;
    case    9600:	myBaud =    B9600 ; break ;
    case   19200:	myBaud =   B19200 ; break ;
    case   38400:	myBaud =   B38400 ; break ;
    case   57600:	myBaud =   B57600 ; break ;
    case  115200:	myBaud =  B115200 ; break ;
    case  230400:	myBaud =  B230400 ; break ;
    case  460800:	myBaud =  B460800 ; break ;
    case  500000:	myBaud =  B500000 ; break ;
    case  576000:	myBaud =  B576000 ; break ;
    case  921600:	myBaud =  B921600 ; break ;
    case 1000000:	myBaud = B1000000 ; break ;
    case 1152000:	myBaud = B1152000 ; break ;
    case 1500000:	myBaud = B1500000 ; break ;
    case 2000000:	myBaud = B2000000 ; break ;
    case 2500000:	myBaud = B2500000 ; break ;
    case 3000000:	myBaud = B3000000 ; break ;
    case 3500000:	myBaud = B3500000 ; break ;
    case 4000000:	myBaud = B4000000 ; break ;

    default:
      return -2 ;
  }

  if ((fd = open (device, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK)) == -1)
    return -1 ;

  fcntl (fd, F_SETFL, O_RDWR) ;

// Get and modify current options:

  tcgetattr (fd, &options) ;

    cfmakeraw   (&options) ;
    cfsetispeed (&options, myBaud) ;
    cfsetospeed (&options, myBaud) ;

    options.c_cflag |= (CLOCAL | CREAD) ;
    options.c_cflag &= ~PARENB ;
    options.c_cflag &= ~CSTOPB ;
    options.c_cflag &= ~CSIZE ;
    options.c_cflag |= CS8 ;
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG) ;
    options.c_oflag &= ~OPOST ;

    options.c_cc [VMIN]  =   0 ;
    options.c_cc [VTIME] = 100 ;	// Ten seconds (100 deciseconds)

  tcsetattr (fd, TCSANOW, &options) ;

  ioctl (fd, TIOCMGET, &status);

  status |= TIOCM_DTR ;
  status |= TIOCM_RTS ;

  ioctl (fd, TIOCMSET, &status);

  usleep (10000) ;	// 10mS

  return fd ;
}


/*
 * serialFlush:
 *	Flush the serial buffers (both tx & rx)
 *********************************************************************************
 */

void serialFlush (const int fd)
{
  tcflush (fd, TCIOFLUSH) ;
}


/*
 * serialClose:
 *	Release the serial port
 *********************************************************************************
 */

void serialClose (const int fd)
{
  close (fd) ;
}


/*
 * serialPutchar:
 *	Send a single character to the serial port
 *********************************************************************************
 */

void serialPutchar (const int fd, const unsigned char c)
{
  int ret;
  ret = write (fd, &c, 1) ;
  if (ret < 0)
  	 printf("Serial Putchar Error\n");
}


/*
 * serialPuts:
 *	Send a string to the serial port
 *********************************************************************************
 */

void serialPuts (const int fd, const char *s)
{ 
	int ret;
	ret = write (fd, s, strlen (s));
	if (ret < 0)
		printf("Serial Puts Error\n");
}

/*
 * serialPrintf:
 *	Printf over Serial
 *********************************************************************************
 */

void serialPrintf (const int fd, const char *message, ...)
{
  va_list argp ;
  char buffer [1024] ;

  va_start (argp, message) ;
    vsnprintf (buffer, 1023, message, argp) ;
  va_end (argp) ;

  serialPuts (fd, buffer) ;
}


/*
 * serialDataAvail:
 *	Return the number of bytes of data avalable to be read in the serial port
 *********************************************************************************
 */

int serialDataAvail (const int fd)
{
  int result ;

  if (ioctl (fd, FIONREAD, &result) == -1)
    return -1 ;

  return result ;
}


/*
 * serialGetchar:
 *	Get a single character from the serial device.
 *	Note: Zero is a valid character and this function will time-out after
 *	10 seconds.
 *********************************************************************************
 */

int serialGetchar (const int fd)
{
  uint8_t x ;

  if (read (fd, &x, 1) != 1)
    return -1 ;

  return ((int)x) & 0xFF ;
}

参考wiringSerial.c撰写自己的串口通信源码:

serialTool.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

int myserialOpen (const char *device, const int baud)
{
    struct termios options ;
    speed_t myBaud ;
    int status, fd ;
    
    switch (baud){
        case 9600: myBaud = B9600 ; break ;
        case 115200: myBaud = B115200 ; break ;
    }

	/* 打开串口驱动文件 */
    if ((fd = open (device, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK)) == -1)
        return -1 ;

	/* 下面为标准操作,设置波特率,奇偶校验位,停止位,数据位等 */
    fcntl (fd, F_SETFL, O_RDWR) ;
    // Get and modify current options:
    tcgetattr (fd, &options) ;
    cfmakeraw (&options) ;
    cfsetispeed (&options, myBaud) ;
    cfsetospeed (&options, myBaud) ;
    options.c_cflag |= (CLOCAL | CREAD) ;
    options.c_cflag &= ~PARENB ;
    options.c_cflag &= ~CSTOPB ;
    options.c_cflag &= ~CSIZE ;
    options.c_cflag |= CS8 ;
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG) ;
    options.c_oflag &= ~OPOST ;
    options.c_cc [VMIN] = 0 ;
    options.c_cc [VTIME] = 100 ; // Ten seconds (100 deciseconds)
    tcsetattr (fd, TCSANOW, &options) ;
    ioctl (fd, TIOCMGET, &status);
    status |= TIOCM_DTR ;
    status |= TIOCM_RTS ;
    ioctl (fd, TIOCMSET, &status);
    usleep (10000) ; // 10mS
    
    return fd ;
}

/* 向串口写入一个字符串 */
void serialSendstring (const int fd, const char *s)
{
    int ret;
    ret = write (fd, s, strlen (s));
    if (ret < 0)
        printf("Serial Puts Error\n");
}

/* 读取串口 32 个字节的字符串 */
int serialGetstring (const int fd, char *buffer)
{
    int n_read;
    n_read = read(fd, buffer,32);
    return n_read;
}

serialTool.h

int myserialOpen (const char *device, const int baud);
void serialSendstring (const int fd, const char *s);
int serialGetstring (const int fd, char *buffer);

测试代码可自行编写,参考⑥ 串口开发的例程,编译时无需再连接wiringPi库。

⑧ 香橙派摄像头的使用:

[1]USB 摄像头插入到 OrangePi 开发板的 USB 接口中,然后通过lsmod命令可以看到内核自动加载了uvcvideo模块:

lsmod | grep uvcvideo | grep -v grep

在这里插入图片描述

[2] 通过v4l2-ctl命令可以看到 USB 摄像头的设备节点信息为/dev/videox(x 有可能是 0、1 或 2 等数字):

sudo apt update
sudo apt install -y v4l-utils

v4l2-ctl --list-devices

如图:
在这里插入图片描述

[3] 使用fswebcam测试摄像头:

sudo apt update
sudo apt-get install -y fswebcam

sudo fswebcam -d /dev/video1 --no-banner -r 1280x720 -S 5 ./image.jpg	# 注意这里的 video1 要根据实际的情况修改

解析:

-d用于指定 USB 摄像头的设备节点
--no-banner用于去除照片的水印
-r用于指定照片的分辨率
-S用于设置跳过前面的帧数
./image.jpg用于设置生成的照片的名字和路径

[4] 使用mjpg-streamer测试 USB 摄像头:

 安装mjpg-streamer

git clone https://github.com/jacksonliam/mjpg-streamer		# Github 的下载地址git clone https://gitee.com/leeboby/mjpg-streamer			# Gitee 的镜像下载地址(推荐)

安装依赖的软件包,Ubuntu 系统使用此命令:

sudo apt-get install -y cmake libjpeg8-dev

编译安装mjpg-streamer

cd mjpg-streamer/mjpg-streamer-experimental

make -j4
sudo make install

mjpg-streamer/mjpg-streamer-experimental目录下,打开start.sh,修改前排的相应语句,注意是否为video1,改为:

./mjpg_streamer -i "./input_uvc.so -d /dev/video1 -u -f 30" -o "./output_http.so -w ./www"

保存并执行./start.sh,启动mjpg服务;

然后在和开发板同一局域网的 Ubuntu PCWindows PC 或手机的浏览器中输入【开发板的 IP地址:8080】192.168.xx.xxx:8080就能看到摄像头输出的视频了。拍照命令:

wget http://192.168.xx.xxx:8080/?action=snapshot -O /存放路径/image.jpg

[5]mjpg-streamer服务开机自启动:

 在工作目录中创建一个mjpg.sh可执行文件,添加以下启动脚本:

#!/bin/bash

cd /home/orangepi/mjpg-streamer/mjpg-streamer-experimental
./start.sh

添加可执行权限:chmod +x mjpg.sh

然后配置开机启动,先进入文件夹:

cd /etc/xdg/autostart

随便复制一个已有的文件(可以确保权限等符合要求),命名为mjpg.desktop,比如im-launch.desktop文件:

sudo cp im-launch.desktop mjpg.desktop

mjpg.desktop内容为:

[Desktop Entry]
Name=mjpg
Exec=/home/orangepi/mjpg.sh
Type=Application
NoDisplay=true

重启 OrangePi 开发板

4、Linux 的热拔插 UDEV 机制

  • 当一个新的设备接入开发板的USB时,dmesg能够查看到接入的设备信息,我们使用adb工具却无法访问到新设备,输入adb devices会出现提醒:dinsufficient permissions for device: user in plugdev group; are your udev rules wrong?(使用adb前请先安装adb工具:sudo apt-get install adb);此时,我们要配置文件以支持USB设备的热拔插、支持UDEV的机制:在/etc/udev/rules.d文件夹下创建规则文件,sudo vim xxx.rules,在文件中添加内容SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666",再重新拔插USB设备(如果是手机还需要打开手机的开发者模式,打开USB调试,在手机上确认手机调试模式)
  • udev是一个设备管理工具,udev以守护进程的形式运行,通过侦听内核发出来的uevent来管理/dev目录下的设备文件。udev在用户空间运行,而不在内核空间运行。它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下。使用udev后,在/dev目录下就只包含系统中真正存在的设备
① 守护进程:
  • Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogdweb服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。守护进程的名称通常以d结尾
  • UDEV守护进程,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等
  • 守护进程的基本特点:① 生存周期长,一般操作系统启动的时候就启动,关闭的时候关闭;② 守护进程和终端无关联,也就是他们没有控制终端,所以当控制终端退出,也不会导致守护进程退出;③ 守护进程是在后台运行,不会占着终端,终端可以执行其他命令;④ 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程
  • linux操作系统本身是有很多的守护进程在默默执行,维持着系统的日常活动。大概 30-50 个

在 Linux 系统中,可以通过命令ps -elfps -efj来查看守护进程:

在这里插入图片描述

  • ppid = 0:内核进程,跟随系统启动而启动,生命周期贯穿整个系统;
  • CMD列中名字带[]的,叫内核守护进程
  • CMD列中名字不带[]的普通守护进程(用户集守护进程)
  • init进程:也是系统守护进程,它负责启动各运行层次特定的系统服务,所以很多进程的PPIDinit,也负责收养孤儿进程
② 守护进程开发:
#include <unistd.h>

int daemon(int nochdir, int noclose);
返回值:成功返回 0,失败返回 -1
nochdir参数:0时表示将当前目录更改至/
noclose参数:0时表示将标准输入、标准输出、标准错误重定向至/dev/null

解析:由于守护进程跟随系统启动而启动,它不依托于终端,它的工作目录、启动者可能是root用户,不与应用层普通用户产生联系,所以要将工作目录切换至根目录。且因为有此特点,所以不便于操作守护进程,此时可以使用信号的方式来操控该进程

例程timedaemon.c

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdbool.h>

/*
	#include <time.h>
	char *asctime(const struct tm *timeptr);	将 timeptr 结构体表示的时间用字符串的形式表示

	struct tm *localtime(const time_t *timer);	使用 timer 的值来填充 tm 结构,timer 的值被分解为 tm 结构,并用本地时区表示

	time_t time(time_t *tloc);	返回自 Unix 纪元 ( January 1 1970 00:00:00 GMT ) 起的当前时间的秒数

struct tm{
   int tm_sec;		秒,范围从 0 到 59
   int tm_min;		分,范围从 0 到 59
   int tm_hour;		小时,范围从 0 到 23
   int tm_mday;		一月中的第几天,范围从 1 到 31
   int tm_mon;		月份,范围从 0 到 11
   int tm_year;		自 1900 起的年数
   int tm_wday;		一周中的第几天,范围从 0 到 6
   int tm_yday;		一年中的第几天,范围从 0 到 365
   int tm_isdst;	夏令时
};

*/

static bool flag = true;

void handler(int sig)			// 信号处理函数
{
    printf("I got a signal %d\nI'm quitting.\n", sig);
    flag = false;
}

int main()
{
    time_t t;
    int fd;

    // 创建守护进程
    if(-1 == daemon(0, 0))
    {
        printf("daemon error\n");
        exit(1);
    }

    // 设置信号处理函数
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if(sigaction(SIGQUIT, &act, NULL))
    {
        printf("sigaction error.\n");
        exit(0);
    }

    // 进程工作内容
    while(flag)
    {
        fd = open("/home/orangepi/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
        if(fd == -1)
        {
            printf("open error\n");
        }
        t = time(0);								// 获取时间
        char *buf = asctime(localtime(&t));			// 翻译时间结构体
        write(fd, buf, strlen(buf));
        close(fd);
        sleep(10);
    }

    return 0;
}

可以查询到我们创建的守护进程:
在这里插入图片描述

设置开机自启动,在/etc/rc.local文件中添加执行路径:
在这里插入图片描述

sudo vi /etc/rc.local

在这里插入图片描述
sudo reboot重启开发板,可以看到我们的守护进程:

在这里插入图片描述

③ 守护进程应用:

守护进程使某程序一直运行,防止应用程序崩溃意外退出,例程:

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdbool.h>

static bool flag = true;

void handler(int sig)
{
    printf("I got a signal %d\nI'm quitting.\n", sig);
    flag = false;
}

int judMent()           // 判断需要守护的进程 xxxx 是否正在运行
{
    FILE *file;
    char buffer[128] = {'\0'};
    char *cmd = "ps -elf |grep xxxx|grep -v grep";			// 查找 xxxx 进程切忽略 grep xxxx 进程
    file = popen(cmd, "r");									// 保存终端输出
    fgets(buffer, 128, file);								// 获取终端输出
    if(strstr(buffer, "xxxx") != NULL){
        return 0;
    }else{
        return -1;
    }
    printf("BUFFER:%s\n",buffer);
}

int main()
{
    time_t t;
    int fd;

    //创建守护进程
    if(-1 == daemon(0, 0))
    {
        printf("daemon error\n");
        exit(1);
    }

    //设置信号处理函数
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if(sigaction(SIGQUIT, &act, NULL))
    {
        printf("sigaction error.\n");
        exit(0);
    }

    //进程工作内容
    while(flag)
    {
        if( judMent() == -1){
            system("/home/orangepi/xxxx &");
            // 启动程序,& 号表示运行为后台进程
        }
        sleep(2);
    }

    return 0;
}

并设置开机启动:
编辑/etc/rc.local文件:sudo vi /etc/rc.local

添加以下字段:

/home/orangepi/xDaemon
/home/orangepi/xxxx &

exit 0
④ UDEV 的配置文件:
  • 规则文件是udev里最重要的部分,默认是存放在/etc/udev/rules.d/下,所有的规则文件必须以rules为后缀名
  • 例如下面一个简单的规则:KERNEL=="sda", NAME="my_root_disk", MODE="0660"KERNEL 是匹配键,NAMEMODE 是赋值键。这条规则的意思是:如果有一个设备的内核名称为sda,则该条件生效,并执行后面的赋值:在/dev下产生一个名为my_root_disk的设备文件,并把设备文件的权限设为0660
  • 当我们接入 USB 设备后,可以通过dmesg来查看相应的信息,可以看到new high-speed USB device下的number 28
    在这里插入图片描述
    相应地,在/dev/bus/总线下的usb设备下的001生成了028
    在这里插入图片描述
    我们可以使用udevadm info --attribute-walk --name=/dev/设备名字指令查看更多消息:udevadm info --attribute-walk --name=/dev/bus/usb/001/028,我们可以通过这些信息来进行规则匹配,如:
    在这里插入图片描述     在这里插入图片描述
    我们可以将 4、Linux 的热拔插 UDEV 机制 的概述中在/etc/udev/rules.d/下创建的规则文件的字段改写为:
    SUBSYSTEM=="usb", ATTRS{idVendor}=="12d1", ATTRS{idProduct}=="107e", MODE="0666",依然适用
  • udev规则的匹配键:

    ACTION: 事件(uevent)的行为,例如:add(添加设备)、remove(删除设备);
    KERNEL: 内核设备名称,例如:sdacdrom
    DEVPATH: 设备的 devpath 路径;
    SUBSYSTEM: 设备的子系统名称,例如:sda 的系统为 block
    BUS: 设备在 devpath 里的总线名称,例如:usb
    DRIVER: 设备在 devpath 的设备驱动名称,例如:ide-cdrom
    ID: 设备在 devpath 里的识别号;
    SYSFS{filename}: 设备的 devpath 路径下,设备的属性文件 “filename” 里的内容;
    ENV{key}: 环境变量,在一条规则中,可以设定最多五条环境变量的 匹配键;
    PROGRAM: 调用外部命令;
    RESULT: 外部命令 PROGRAM 的返回结果
⑤ UDEV 自动挂载 U 盘:

/etc/udev/rules.d/添加规则文件usbflashdisk.rules,内容如下:

ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", RUN{program}+="/bin/mkdir /media/%k", RUN{program}+="/usr/bin/systemd-mount --no-block --collect $devnode /media/%k"

使用dmesg查看,挂载到的目录(/media/sda//media/sdb下):
在这里插入图片描述
可以看到,挂载到了/media/sda1

三、智能垃圾分类垃圾桶项目

1、功能需求:

  • 语音控制开启垃圾分类识别,并触发垃圾桶的开关盖
  • Sockect发送指令远程控制开启垃圾分类识别,并触发垃圾桶的开关盖
  • 图像识别垃圾分类功能
  • 语音播报垃圾类型
  • OLED显示垃圾物品类型
  • 根据垃圾类型开关不同类型垃圾桶
  • 图像处理使用阿里SDK,支持PythonJava接口

2、安装 python 环境(需要安装 python3.9 或以上版本):

3、C 语言调用 Python:

① 安装 libpython3-dev 依赖包:

先检查是否安装了libpython3dev依赖包:

dpkg -l | grep libpython3 | grep dev

更新软件包,并安装libpython3.10dev依赖包:

sudo apt-get update
sudo apt install libpython3.10-dev

确认安装是否完成:

dpkg -l | grep libpython3 | grep dev
② 直接调用 python 语句:

一个简单的simple.c例子:

#include "Python.h"

int main()
{
	Py_Initialize();		// 初始化
	PyRun_SimpleString("print ('funny')");
	Py_Finalize();			//释放资源

	return 0;
}

编译:

gcc simple.c -o simple -I /usr/include/python3.10 -l python3.10

解析:

  • Python.h头文件,是包含 Python API 的头文件,用于访问 Python 对象和函数
  • 程序开始时使用Py_Initialize()函数初始化 Python 解释器,这样可以在 C 程序中执行 Python 代码
  • int PyRun_SimpleString(const char *command);函数说明:
      传递一个字符串作为参数,表示要执行的 Python 代码,如print ('funny')
      这是针对下面PyRun_SimpleStringFlags()的简化版接口,将PyCompilerFlags*参数设为NULL,传参就是 python 执行语句
  • 在程序结束时使用Py_Finalize()函数关闭 Python 解释器,并释放资源
③ 调用无参 python 函数:
一般调用的流程如下:


1、包含Python.h头文件,以便使用 Python API

2、使用void Py_Initialize()初始化 Python 解释器

3、使用PyObject *PyImport_ImportModule(const char *name)PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)获取sys.path对象,并利用int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path中,以便加载当前的 Python 模块 ( Python 文件即 python 模块)

4、使用PyObject *PyImport_ImportModule(const char *name)函数导入 Python 模块,并判断是否有出错

5、使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取 Python 函数对象,并判断是否有出错

6、使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用 Python 函数,并获取返回值

7、使用void Py_DECREF(PyObject *o)函数释放所有引用的 Python 对象

8、结束时调用void Py_Finalize()函数关闭 Python 解释器

相关的函数参数说明参考网站,🔗点击这里

欲要调用以下nopara.py模块的say_funny无参无返回值函数:

def say_funny():
    print('funny')

示例:

#include <Python.h>

int main()
{
    Py_Initialize();    // 初始化

    // 将当前路径添加到 sys.path 中
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(path, PyUnicode_FromString("."));		// PyUnicode_FromString() 将 c 语言的字符串转为 python 的字符串

    // 导入 nopara 模块
    PyObject *pModule = PyImport_ImportModule("nopara");
    if (!pModule)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: failed to load nopara.py\n");
        return 1;
    }

    // 获取 say_funny 函数对象
    PyObject *pFunc = PyObject_GetAttrString(pModule, "say_funny");
    if (!pFunc || !PyCallable_Check(pFunc))
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function say_funny not found or not callable\n");
        return 1;
    }
    
    // 调用 say_funny 函数并获取返回值
    PyObject *pValue = PyObject_CallObject(pFunc, NULL);	// 无参,置 NULL
    if (!pValue)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function call failed\n");
        return 1;
    }

    // 释放所有引用的 Python 对象,最后获取的 pValue 最先释放
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
    Py_DECREF(pModule);
    
    // 关闭Python解释器
    Py_Finalize();
    
    return 0;
}

编译:

gcc nopara.c -o nopara -I /usr/include/python3.10 -l python3.10

运行结果:

funny
④ 调用有参 python 函数:
一般调用的流程如下:


1、包含Python.h头文件,以便使用 Python API

2、使用void Py_Initialize()初始化 Python 解释器

3、使用PyObject *PyImport_ImportModule(const char *name)PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)获取sys.path对象,并利用int PyList_Append(PyObject *list, PyObject *item)将当前路径.添加到sys.path中,以便加载当前的 Python 模块 ( Python 文件即 python 模块)

4、使用PyObject *PyImport_ImportModule(const char *name)函数导入 Python 模块,并判断是否有出错

5、使用PyObject *PyObject_GetAttrString(PyObject *o, const char *attr_name)函数获取 Python 函数对象,并判断是否有出错

6、使用PyObject *Py_BuildValue(const char *format, ...)函数创建一个 python 元组,将 C 类型的数据结构转换成 Python 对象,作为 Python 函数的参数,没有参数不需要调用

  参数format对应的PythonC/C++类型如下:

在这里插入图片描述
  下面PyArg_Parse()函数的format参数同理

7、使用PyObject *PyObject_CallObject(PyObject *callable, PyObject *args)函数调用 Python 函数,并获取返回值

8、使用int PyArg_Parse(PyObject *args, const char *format, ...)函数将返回值转换为 C 类型,并判断是否有出错,没有返回值时不需要调用

9、使用void Py_DECREF(PyObject *o)函数释放所有引用的 Python 对象

10、结束时调用void Py_Finalize()函数关闭 Python 解释器

相关的函数参数说明参考网站,🔗点击这里

欲要调用以下para.py模块的say_funny有参有返回值函数:

def say_funny(s):
    print(s, 'is funny')
    return s

示例:

#include <Python.h>

int main()
{
    Py_Initialize();    // 初始化

    // 将当前路径添加到 sys.path 中
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(path, PyUnicode_FromString("."));		// PyUnicode_FromString() 将 c 语言的字符串转为 python 的字符串

    // 导入 nopara 模块
    PyObject *pModule = PyImport_ImportModule("para");
    if (!pModule)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: failed to load para.py\n");
        return 1;
    }

    // 获取 say_funny 函数对象
    PyObject *pFunc = PyObject_GetAttrString(pModule, "say_funny");
    if (!pFunc || !PyCallable_Check(pFunc))
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function say_funny not found or not callable\n");
        return 1;
    }
    
	// 将 C 类型的数据结构转换成 Python 对象
	char *str = "Embedded";
	PyObject *pArgs = Py_BuildValue("(s)", str);		// () 代表元组

    // 调用 say_funny 函数并获取返回值
    PyObject *pValue = PyObject_CallObject(pFunc, pArgs);		// 同时传参
    if (!pValue)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function call failed\n");
        return 1;
    }

	// 将返回值转换为 C 类型
	char *result = NULL;
	if( !PyArg_Parse(pValue, "s", &result) ){
		PyErr_Print();      // python 中打印错误信息
        printf("ERROR: parse failed\n");
        return 1;
	}
	printf("result = %s\n", result);

    // 释放所有引用的 Python 对象,最后获取的 pValue 最先释放
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
    Py_DECREF(pModule);
    
    // 关闭Python解释器
    Py_Finalize();
    
    return 0;
}

编译:

gcc para.c -o para -I /usr/include/python3.10 -l python3.10

运行结果:

Embedded is funny
result = Embedded

4、阿里云垃圾分类识别测试:

根据 阿里云官网垃圾分类识别🔗点击这里 的接入指引确认好已完成准备工作,并且在OrangePi上完成Python SDK的安装,我们需要安装 图像识别SDK包,根据官网上的命令(注意该命令时效性):

pip install alibabacloud_imagerecog20190930

前提是已经安装了python3-pip,安装命令:

sudo apt install python3-pip


继续根据官网指示,配置环境变量,在<>位置输入自己的阿里云AccessKey IDAccessKey Secret(注意使用双引号"",不使用尖括号<>):

export ALIBABA_CLOUD_ACCESS_KEY_ID=<access_key_id> 
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=<access_key_secret>


🔗点击这里 拷贝 “文件在本地或可访问的URL” 的示例代码,我们使用场景一的代码部分,注释场景二的部分代码,还要修改本地图片的索引路径:

# -*- coding: utf-8 -*-
# 引入依赖包
# pip install alibabacloud_imagerecog20190930

import os
import io
from urllib.request import urlopen
from alibabacloud_imagerecog20190930.client import Client
from alibabacloud_imagerecog20190930.models import ClassifyingRubbishAdvanceRequest
from alibabacloud_tea_openapi.models import Config
from alibabacloud_tea_util.models import RuntimeOptions

config = Config(
  # 创建AccessKey ID和AccessKey Secret,请参考https://help.aliyun.com/document_detail/175144.html。
  # 如果您用的是RAM用户的AccessKey,还需要为RAM用户授予权限AliyunVIAPIFullAccess,请参考https://help.aliyun.com/document_detail/145025.html
  # 从环境变量读取配置的AccessKey ID和AccessKey Secret。运行代码示例前必须先配置环境变量。
  access_key_id=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID'),
  access_key_secret=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
  # 访问的域名
  endpoint='imagerecog.cn-shanghai.aliyuncs.com',
  # 访问的域名对应的region
  region_id='cn-shanghai'
)

# 场景一:文件在本地
img = open(r'/tmp/ClassifyingRubbish1.jpg', 'rb')
# 场景二:使用任意可访问的url
# url = 'https://viapi-test-bj.oss-cn-beijing.aliyuncs.com/viapi-3.0domepic/imagerecog/ClassifyingRubbish/ClassifyingRubbish1.jpg'
# img = io.BytesIO(urlopen(url).read())
classifying_rubbish_request = ClassifyingRubbishAdvanceRequest()
classifying_rubbish_request.image_urlobject = img
runtime = RuntimeOptions()
try:
  # 初始化Client
  client = Client(config)
  response = client.classifying_rubbish_advance(classifying_rubbish_request, runtime)
  # 获取整体结果
  print(response.body)
except Exception as error:
  # 获取整体报错信息
  print(error)
  # 获取单个字段
  print(error.code)

准备好图片放在相应路径,运行:

python3 garbage.py

运行结果:

{'Data': {'Elements': [{'Category': '干垃圾', 'CategoryScore': 0.6612, 'Rubbish': '塑料袋', 'RubbishScore': 0.6612}], 'Sensitive': False}, 'RequestId': '3AD473CA-F6FA-56E6-AA5F-62FD70CB6661'}

5、C 语言调用阿里云 python 接口:

① 添加永久环境变量:

打开:

vi ~/.bashrc

在文件末尾加入添加环境变量的语句:

export ALIBABA_CLOUD_ACCESS_KEY_ID=<access_key_id> 
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=<access_key_secret>
② python 接口返回值类型检查:

从上面运行的结果我们可以看到,接口返回的结果是一个多层嵌套的字典格式,但是返回的类型是否的dict类型的数据呢?

我们可以做一个测试,对接口返回的数据类型进行判断,代码修改:

在这里插入图片描述

运行结果:

<class 'alibabacloud_imagerecog20190930.models.ClassifyingRubbishResponseBody'>

发现接口返回结果是一个ClassifyingRubbishResponseBody类型,而不是dict类型,而是一个类

所以我们接下来需要将ClassifyingRubbishResponseBody数据类型转化为dict类型:

  • 第一步,我们需要查看安装的 图像识别 Python3 SDK包安装的路径,我们曾经使用这个安装命令:pip install alibabacloud_imagerecog20190930,现在我们再执行一次,可以捕捉到:

    在这里插入图片描述
    我们进入该目录cd /home/orangepi/.local/lib/python3.10/site-packages,然后搜索类型grep -r ClassifyingRubbishResponseBody

    在这里插入图片描述
    可以看到,在alibabacloud_imagerecog20190930/models.py模块中找到了该类,我们进入该模块vi alibabacloud_imagerecog20190930/models.py,我们发现有一个to_map()函数(Python 中的Map(‌映射)‌通常指的是字典(‌dict)‌数据类型),我们可以尝试使用该函数,修改代码:

    在这里插入图片描述
    运行结果:
<class 'dict'>
  • 可以看到,已经成功将ClassifyingRubbishResponseBody类型转换为dict类型
③ 示例:

garbage.py模块:

# -*- coding: utf-8 -*-
# 引入依赖包
# pip install alibabacloud_imagerecog20190930

import os
import io
from urllib.request import urlopen
from alibabacloud_imagerecog20190930.client import Client
from alibabacloud_imagerecog20190930.models import ClassifyingRubbishAdvanceRequest
from alibabacloud_tea_openapi.models import Config
from alibabacloud_tea_util.models import RuntimeOptions

config = Config(
  # 创建AccessKey ID和AccessKey Secret,请参考https://help.aliyun.com/document_detail/175144.html。
  # 如果您用的是RAM用户的AccessKey,还需要为RAM用户授予权限AliyunVIAPIFullAccess,请参考https://help.aliyun.com/document_detail/145025.html
  # 从环境变量读取配置的AccessKey ID和AccessKey Secret。运行代码示例前必须先配置环境变量。
  access_key_id=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID'),
  access_key_secret=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
  # 访问的域名
  endpoint='imagerecog.cn-shanghai.aliyuncs.com',
  # 访问的域名对应的region
  region_id='cn-shanghai'
)

def alibaba_garbage():
    #场景一:文件在本地
    img = open(r'/tmp/garbage.jpg', 'rb')
    #场景二:使用任意可访问的url
    #url = 'https://viapi-test-bj.oss-cn-beijing.aliyuncs.com/viapi-3.0domepic/imagerecog/ClassifyingRubbish/ClassifyingRubbish1.jpg'
    #img = io.BytesIO(urlopen(url).read())
    classifying_rubbish_request = ClassifyingRubbishAdvanceRequest()
    classifying_rubbish_request.image_urlobject = img
    runtime = RuntimeOptions()
    try:
      # 初始化Client
      client = Client(config)
      response = client.classifying_rubbish_advance(classifying_rubbish_request, runtime)
      # 获取整体结果
      print(response.body.to_map()['Data']['Elements'][0]['Category'])
      return response.body.to_map()['Data']['Elements'][0]['Category']
    except Exception as error:
      # 不再打印多余报错信息
      return '获取失败'

if __name__ == "__main__":
    alibaba_garbage()

garbage.c文件:

#include <Python.h>

int main()
{
    Py_Initialize();    // 初始化

    // 将当前路径添加到 sys.path 中
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(path, PyUnicode_FromString("."));     // PyUnicode_FromString() 将 c 语言的字符串转为 python 的字符串

    // 导入 garbage 模块
    PyObject *pModule = PyImport_ImportModule("garbage");
    if (!pModule)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: failed to load garbage.py\n");
        return 1;
    }

    // 获取 alibaba_garbage() 函数对象
    PyObject *pFunc = PyObject_GetAttrString(pModule, "alibaba_garbage");
    if (!pFunc || !PyCallable_Check(pFunc))
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function alibaba_garbage not found or not callable\n");
        return 1;
    }

    // 调用 alibaba_garbage 函数并获取返回值
    PyObject *pValue = PyObject_CallObject(pFunc, NULL);        // 无参
    if (!pValue)
    {
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: function call failed\n");
        return 1;
    }

    // 将返回值转换为 C 类型
    char *result = NULL;
    if( !PyArg_Parse(pValue, "s", &result) ){
        PyErr_Print();      // python 中打印错误信息
        printf("ERROR: parse failed\n");
    }
    printf("result = %s\n", result);

    // 释放所有引用的 Python 对象,最后获取的 pValue 最先释放
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
    Py_DECREF(pModule);

    // 关闭Python解释器
    Py_Finalize();

    return 0;
}

编译:

gcc garbage.c -o garbage -I /usr/include/python3.10 -l python3.10

运行结果:

干垃圾
result = 干垃圾

6、项目示例:

① 环境准备:

[1] 将语音模块连接在UART5的位置,参考gpio readall的引脚说明接线

[2]orangepi3.0.6上确认已经配置开启了uart5overlays=uart5,查看配置文件cat /boot/orangepiEnv.txt

在这里插入图片描述

[3]USB 摄像头接到香橙派开发板上,同时确认mjpg服务已经开启

[4] 保留orangepi用户的环境变量:

方法一:

/dev/ttyS5设置为777权限:

sudo chmod 777 /dev/ttyS5

理由是:代码运行需要打开根目录下的串口驱动文件/dev/ttyS5,需要sudo来运行,而我们安装的阿里云SDK是安装在orangepi用户下的.local目录下的,如果我们用sudo来运行代码,则是使用root用户运行,无法调用阿里云SDK,并且会提示我们没有安装阿里云SDK,所以,我们将/dev/ttyS5的权限增加,运行代码时则不需要再使用sudo权限

方法二(推荐):

sudo -E ./main

运行时使用-E设置,意思为保留当前用户的环境变量

② 补充知识点:

一、TCP 心跳机制解决 Soket 异常断开问题:

  • Socket 客户端的断开无非就两种情况:

      1、客户端能够发送状态给服务器;正常断开,强制关闭客户端等,客户端能够做出反应。
      2、客户端不能发送状态给服务器;突然断网,断电,客户端卡死等,客户端根本没机会做出反应,服务器更不了解客户端状态,导致服务器异常等待。


二、为了解决上述问题,引入 TCP 心跳包机制:

  • 心跳包的实现,心跳包就是服务器定时向客户端发送查询信息,如果客户端有回应就代表连接正常,类似于 linux 系统的看门狗机制。心跳包的机制有一种方法就是采用TCP_KEEPALIVE机制,它是一种用于检测 TCP 连接是否存活的机制,它的原理是:在一定时间内没有数据往来时,发送探测包给对方,如果对方没有响应,就认为连接已经断开。TCP_KEEPALIVE机制可以通过设置一些参数来调整,如探测时间间隔、探测次数等。
  • Linux 内核提供了通过sysctl命令查看和配置TCP KeepAlive参数的方法。

        查看当前系统的TCP KeepAlive参数:

        sysctl net.ipv4.tcp_keepalive_time
        sysctl net.ipv4.tcp_keepalive_probes
        sysctl net.ipv4.tcp_keepalive_intvl

        在这里插入图片描述
        修改TCP KeepAlive参数:

      sysctl net.ipv4.tcp_keepalive_time=5
      sysctl net.ipv4.tcp_keepalive_probes=3
      sysctl net.ipv4.tcp_keepalive_intvl=3

  • 但是,如果我们直接修改了操作系统上的这几个参数时间,会影响系统上的其他服务。所以我们不应该修改操作系统的参数,应该在程序代码中实现


三、C 语言实现 TCP KeepAlive 功能:

  • 对于 Socket 而言,可以在程序中通过 socket 选项开启TCP KeepAlive功能,并配置参数。对应的 Socket 选项分别为SO_KEEPALIVETCP_KEEPIDLETCP_KEEPCNTTCP_KEEPINTVL

        int keepalive = 1;    // 开启 TCP KeepAlive 功能
        int keepidle = 5;   // tcp_keepalive_time 5s 内没收到数据开始发送心跳包
        int keepcnt = 3;     // tcp_keepalive_probes 发送心跳包最大次数
        int keepintvl = 3;   // tcp_keepalive_intvl 每 3s 发送一次心跳包

        setsockopt(client_socketfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive, sizeof(keepalive));
        setsockopt(client_socketfd, SOL_TCP, TCP_KEEPIDLE, (void *) &keepidle, sizeof (keepidle));
        setsockopt(client_socketfd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof (keepcnt));
        setsockopt(client_socketfd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof (keepintvl));
③ 代码示例:

见文章:香橙派 “智能垃圾分类识别垃圾桶” 代码示例 🔗点此打开

编译:gcc -o garbage *.c -I /usr/include/python3.10 -l python3.10 -lwiringPi -lwiringPiDev -lpthread -lm -lcrypt -lrt

;