【Linux】mjpg-streamer 源码分析

2023-05-16

文章目录

    • 1.总体流程
    • 2.主进程的源码分析
      • 2.1 参数接收与解析
      • 2.2 获取参数
      • 2.3 调用输入函数
        • 2.3.1 程序手动中断信号
        • 2.3.2 strchr()函数
        • 2.3.3 strndup()函数
        • 2.3.4 分离参数
    • 3.输入通道源码分析
      • 3.1 input_init
      • 3.2 input_run
      • 3.3 cam_thread
    • 4.输出通道源码分析
      • 4.1 output_init
      • 4.2 output_run
      • 4.3 server_thread
      • 4.4 client_thread
      • 4.5 send_stream

1.总体流程

mjpg-streamer Github
mjpg-streamer tree

目录组成:

文件夹名功能
cmakemakefile文件
plugins输入输出组件,用于采集及传输
scripts执行脚本
www用于浏览器功能代码

mjpg-streamer框架:

唤醒
socket
bind
listen
accept
创建client_thread线程
send_stream
pthread_cond_wait 等待仓库数据更新
write 通过socket发送帧图形
dlopen 打开组件
dlsym 获取相关函数
input_init 输入组件初始化
init_videoIn
init_v4l2
output_init 输出组件初始化
input_run
创建cam_thread线程
uvcGrab 获取一帧
memcpy_picture 拷贝图像
图像数据
pthread_cond_broadcast 唤醒发送帧数据
output_run
创建server_thread线程
  • dlopen 打开 输入组件(动态链接库 .so),有以下几种:

    • input_file.so
    • input_http.so
    • input_uvc.so
  • dlopen 打开 输出组件(动态链接库 .so),有以下几种:

    • output_file.so
    • output_http.so
    • output_udp.so
    • output_rtsp.so

mjpg-streamer plugins

2.主进程的源码分析

2.1 参数接收与解析

mjpg_streamer.c 中接收参数分析:

int main(int argc, char *argv[])
{
    //char *input  = "input_uvc.so --resolution 640x480 --fps 5 --device /dev/video0"; //改为 mjpg-steamer 默认的参数
    char *input[MAX_INPUT_PLUGINS];
    char *output[MAX_OUTPUT_PLUGINS];
    int daemon = 0, i, j;
    size_t tmp = 0;

    output[0] = "output_http.so --port 8080";
    global.outcnt = 0;
    global.incnt = 0;

    while(1) {
        int c = 0;
        static struct option long_options[] = {
            {"help", no_argument, NULL, 'h'},
            {"input", required_argument, NULL, 'i'},
            {"output", required_argument, NULL, 'o'},
            {"version", no_argument, NULL, 'v'},
            {"background", no_argument, NULL, 'b'},
            {NULL, 0, NULL, 0}
        };

        c = getopt_long(argc, argv, "hi:o:vb", long_options, NULL);
        if(c == -1) break; // 用于判断是否解析完毕,解析完毕返回 -1
    ... ...

其中用于解析命令的为 getopt_long_only() 函数:

int getopt_long_only(int argc, char * const argv[],
          const char *optstring,
          const struct option *longopts, int *longindex);
  • 参数:

    • argc argv :直接从main函数传递而来,表示传递的参数
    • shortopts:短选项字符串。如”f:v",这里需要指出的是,短选项字符串不需要“-”,而且但选项需要传递参数时,在短选项后面加上“:”。eg.第一个短选项 f 后面带有冒号,: 表示短选项f后面是要指定参数的,如 -f 30。
    • longopts:struct option 数组,用于存放长选项参数。
    • longind:用于返回长选项在 longopts 结构体数组中的索引值,用于调试,一般置为NULL。eg.根据上述源代码传入了 -h 参数,则返回索引值 0。
  • 返回值:

    • 解析完毕,getopt_long_only 返回 -1
    • 出现未定义的长选项或者短选项,getopt_long 返回 ?

2.2 获取参数

        switch(c) {
        case 'i':
            input[global.incnt++] = strdup(optarg);
            break;

        case 'o':
            output[global.outcnt++] = strdup(optarg);
            break;

        case 'v':
            printf("MJPG Streamer Version: %s\n",SOURCE_VERSION);
            return 0;
            break;

        case 'b': // 后台模式
            daemon = 1;
            break;

        case 'h': /* fall through */
        default:
            help(argv[0]);
            exit(EXIT_FAILURE);
        }

当输入如下命令时:

mjpg_streamer -i "input_uvc.so -f 30 -r 1080*720" -o "output_http.so -w www"

接收到 -i 参数后,strdup(optarg) 用来获取相应的标记后的参数,即:

input[global.incnt++]  指向 "input_uvc.so -f 30 -r 1080*720" 字符串

strdup() 函数是 c 语言中常用的一种字符串拷贝库函数:
char *strdup(const char *s);

接收到 -o 参数,同理:

output[global.outcnt++] 指向  "output_http.so -w www"字符串

2.3 调用输入函数

dlopen() 函数负责打开 intput_uvc.so 插件, dlsym() 负责调用插件中的相关函数。

    /* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
    signal(SIGPIPE, SIG_IGN); // 用于忽略 SIGPIPE 信号

    /* register signal handler for <CTRL>+C in order to clean up */
    if(signal(SIGINT, signal_handler) == SIG_ERR) { // SIGINT信号代表由InterruptKey产生 ,当按下 ctrl+c时,调用 signal_handler,做清理工作
        ... ...
    }
    ... ...
    /* open input plugin */
    for(i = 0; i < global.incnt; i++) {
        /* this mutex and the conditional variable are used to synchronize access to the global picture buffer 即用于传输新的帧信号 */
        if(pthread_mutex_init(&global.in[i].db, NULL) != 0) { 
            ... ...
        }
        if(pthread_cond_init(&global.in[i].db_update, NULL) != 0) {
            ... ...
        }

        tmp = (size_t)(strchr(input[i], ' ') - input[i]); // tmp = "input_uvc.so" 字符串的长度
        global.in[i].stop      = 0;
        global.in[i].context   = NULL;
        global.in[i].buf       = NULL;
        global.in[i].size      = 0;
        global.in[i].plugin = (tmp > 0) ? strndup(input[i], tmp) : strdup(input[i]); // 复制前 tmp 的字符,即 global.in[i].plugin = "input_uvc.so" 
        global.in[i].handle = dlopen(global.in[i].plugin, RTLD_LAZY); // 打开  "input_ucv.so" 动态链接库       
        if(!global.in[i].handle) {
            ... ...
        }
        global.in[i].init = dlsym(global.in[i].handle, "input_init"); // global.in[i].init = "input_ucv.c" 里面的 input_init 函数
        ... ...
        global.in[i].stop = dlsym(global.in[i].handle, "input_stop"); // global.in[i].stop = "input_ucv.c" 里面的 input_stop 函数
        ... ...
        global.in[i].run = dlsym(global.in[i].handle, "input_run"); // global.in[i].run = "input_ucv.c" 里面的 input_run 函数
        ... ...
        /* try to find optional command */
        global.in[i].cmd = dlsym(global.in[i].handle, "input_cmd"); // global.in[i].cmd = "input_ucv.c" 里面的 input_cmd 函数

        global.in[i].param.parameters = strchr(input[i], ' '); // 参数字符串为 ' ' 后面的内容,即global.in[i].param.parameters = " -f 30 -r 1080*720"

        for (j = 0; j<MAX_PLUGIN_ARGUMENTS; j++) {
            global.in[i].param.argv[j] = NULL;
        }

        split_parameters(global.in[i].param.parameters, &global.in[i].param.argc, global.in[i].param.argv); // 分割参数,方便后续使用
        global.in[i].param.global = &global;
        global.in[i].param.id = i;

        if(global.in[i].init(&global.in[i].param, i)) { // 调用input_uvc.c 中的 input_init 函数
            LOG("input_init() return value signals to exit\n");
            closelog();
            exit(0);
        }
    }
    
    /* start to read the input, push pictures into global buffer */
    DBG("starting %d input plugin\n", global.incnt);
    for(i = 0; i < global.incnt; i++) {
        ... ...
        if(global.in[i].run(i)) { // 启动读取数据
            ... ...
        }
    }
    for(i = 0; i < global.outcnt; i++) {
        ... ...
        global.out[i].run(global.out[i].param.id);
    }
    
    /* wait for signals */
    pause(); // 等待信号

输出组件同理

2.3.1 程序手动中断信号

    /* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */
    signal(SIGPIPE, SIG_IGN); // 用于忽略 SIGPIPE 信号

    /* register signal handler for <CTRL>+C in order to clean up */
    if(signal(SIGINT, signal_handler) == SIG_ERR) { // SIGINT信号代表由InterruptKey产生 ,当按下 ctrl+c时,调用 signal_handler,做清理工作

当按下 ctrl+c时,调用 signal_handler 函数用于做程序中断后的清理工作。

2.3.2 strchr()函数

char *strchr(const char *str, int c)
  • str :要被检索的字符串
  • c : 在 str 中要搜索的字符

该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。

示例测试:

#include <stdio.h>
#include <string.h>
int main (int argc, char **argv)
{
    const char *input = "input_uvc.so -f 30 -r 1080*720";
    int tmp = (strchr(input, ' ') - input); // tmp = "input_uvc.so" 字符串的长度
    printf("input: %d, ' ': %d\n", input, strchr(input, ' '));
    printf("tmp = %d \n", tmp);
    printf("剩余的字符串 =%s \n", strchr(input, ' '));
    return 0;
}
$ .\strchr
input: 6422189, ' ': 6422201
tmp = 12
剩余的字符串 = -f 30 -r 1080*720

2.3.3 strndup()函数

char* strndup(const char* src, size_t len);

拷贝前 len 个字符,还自动为新的字符串添加一个’\0’表示结尾,返回 该新字符串的地址

2.3.4 分离参数

static int split_parameters(char *parameter_string, int *argc, char **argv)
{
    int count = 1;
    argv[0] = NULL; // the plugin may set it to 'INPUT_PLUGIN_NAME'
    if(parameter_string != NULL && strlen(parameter_string) != 0) {
        char *arg = NULL, *saveptr = NULL, *token = NULL;

        arg = strdup(parameter_string); // 拷贝,arg = " -f 30 -r 1080*720"

        if(strchr(arg, ' ') != NULL) {
            token = strtok_r(arg, " ", &saveptr); // 按照 " " 分割字符串
            if(token != NULL) {
                argv[count] = strdup(token); // argv[1] = "-f 
                count++;
                while((token = strtok_r(NULL, " ", &saveptr)) != NULL) { // 一直按照 " " 分割字符串,直到分割完毕
                    argv[count] = strdup(token); // argv[count++] = 后续参数
                    count++;
                    if(count >= MAX_PLUGIN_ARGUMENTS) {
                        IPRINT("ERROR: too many arguments to input plugin\n");
                        return 0;
                    }
                }
            }
        }
        free(arg);
    }
    *argc = count; // 保存参数个数
    return 1;
}

3.输入通道源码分析

input_uvc.c(plugins\input_uvc) 进行分析

3.1 input_init

int input_init(input_parameter *param, int id)

根据 main 函数传递的参数进行设置:

主要为 指定 USB 摄像头设备、分辨率、帧率、格式、质量、请求buf,队列buf以及一些其他的图像参数。

input_init 输入组件初始化
init_videoIn
init_v4l2

3.2 input_run

input_run
pthread_create 创建 cam_thread 线程
pthread_detach
uvcGrab 获取一帧
memcpy_picture 拷贝图像
int input_run(int id)
{
    input * in = &pglobal->in[id];
    context *pctx = (context*)in->context;
    // 给仓库分配一帧的空间
    in->buf = malloc(pctx->videoIn->framesizeIn);
    ... ...
    // 创建 cam_thread 线程
    pthread_create(&(pctx->threadID), NULL, cam_thread, in);
    // 等待线程执行完,然后回收其资源
    pthread_detach(pctx->threadID);
    return 0;
}

3.3 cam_thread

void *cam_thread(void *arg)
{
    ... ...
    // 当线程执行完后,会调用 cam_cleanup,做一些清理回收工作
    pthread_cleanup_push(cam_cleanup, in);
    ... ...
    // 使能视频捕获设备
    if (video_enable(pcontext->videoIn)) {
        ... ...
    }
    // 当 pglobal->stop = 0时,一直执行while,当按下 Crtl+C时(signal_handler函数),pglobal->stop = 1,停止执行
    while(!pglobal->stop) {
        while(pcontext->videoIn->streamingState == STREAMING_PAUSED) {
            usleep(1); // maybe not the best way so FIXME
        }
        ... ...
        if (FD_ISSET(pcontext->videoIn->fd, &rd_fds)) {
            // 获取一帧数据
            if(uvcGrab(pcontext->videoIn) < 0) {
                ... ...
            }
        ... ...
        }
    }
}

4.输出通道源码分析

input_uvc.c(plugins\output_http) 进行分析

4.1 output_init

int output_init(output_parameter *param, int id)
{
    ... ...
    servers[param->id].id = param->id;
    servers[param->id].pglobal = param->global;
    servers[param->id].conf.port = port;
    servers[param->id].conf.hostname = hostname;
    servers[param->id].conf.credentials = credentials;
    servers[param->id].conf.www_folder = www_folder;
    servers[param->id].conf.nocommands = nocommands;
    ... ...
}

根据 main 函数传递的参数进行设置,省略的部分为解析命令参数,可以看到该函数主要为 给相关变量进行赋值

主要为 指定 端口号、IP地址、文件路径等

4.2 output_run

socket
bind
listen
accept
pthread_create 创建client_thread线程
send_stream
pthread_cond_wait 等待仓库数据更新
write 发送帧图形
output_run
pthread_create 创建server_thread线程
int output_run(int id)
{
    ... ...
    /* create thread and pass context to thread function */
    pthread_create(&(servers[id].threadID), NULL, server_thread, &(servers[id]));
    // 等待线程结束,以便回收资源
    pthread_detach(servers[id].threadID);

    return 0;
}

同理,也是创建一个线程 server_thread

4.3 server_thread

void *server_thread(void *arg)
{
    ... ...
    // 当线程结束时,会调用 server_cleanup 进行相关的清理工作
    pthread_cleanup_push(server_cleanup, pcontext);
    ... ...
    /* 以下为 socket网络编程 创建并连接客户端 */
    ... ...
}

该线程主要为 创建并连接客户端,并创建 client_thread

4.4 client_thread

void *client_thread(void *arg)
{
    ... ...
    // iobuf清零
    init_iobuffer(&iobuf);
    // http协议,需要客户端给服务器发送一个请求,因此初始化一个req
    init_request(&req);
    ... ...
    // 从客户端读取一行数据,以换行符为结束
    if((cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1) {
        ... ...
    }
    ... ...
    // 如果请求字符串为"GET /?action=snapshot",修改请求类型为 拍照
    if(strstr(buffer, "GET /?action=snapshot") != NULL) {
        req.type = A_SNAPSHOT; 
        ... ...
    }
    ... ...
    // 如果请求字符串为"GET /?action=stream",修改请求类型为 stream 视频流
    else if(strstr(buffer, "GET /?action=stream") != NULL) {
        req.type = A_STREAM;
        ... ...
    }
    ... ...
    do {
        ... ...
        // 再一次从客户端读取一行数据
        if((cnt = _readline(lcfd.fd, &iobuf, buffer, sizeof(buffer) - 1, 5)) == -1) {
            ... ...
        }
        // 解析buffer,若其中包含 用户名,则将用户名保存至req.client
        if(strcasestr(buffer, "User-Agent: ") != NULL) {
            req.client = strdup(buffer + strlen("User-Agent: "));
        } 
        // 如何包含了密码,则将密码保存至req.credentials
        else if(strcasestr(buffer, "Authorization: Basic ") != NULL) {
            req.credentials = strdup(buffer + strlen("Authorization: Basic "));
            // 对密码进行解码
            decodeBase64(req.credentials);
            ... ...
        }
    // 字符串<=2字节(除回车换行),不使用该功能
    } while(cnt > 2 && !(buffer[0] == '\r' && buffer[1] == '\n'));

    // 根据不同的请求,进行对应的操作
    switch(req.type) {
        case A_SNAPSHOT_WXP:
        case A_SNAPSHOT:
            send_snapshot(&lcfd, input_number);
            break;
        case A_STREAM:
            send_stream(&lcfd, input_number);
            ... ...
    }
}

_readline 函数用于读取 client发送了什么请求,因此 客户端必须发送一个字符串,以换行符为结束。

4.5 send_stream

void send_stream(cfd *context_fd, int input_number)
{
    ... ...
    sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
            "Access-Control-Allow-Origin: *\r\n" \
            STD_HEADER \
            "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
            "\r\n" \
            "--" BOUNDARY "\r\n");
    // 发送报文头
    if(write(context_fd->fd, buffer, strlen(buffer)) < 0) {
        ... ...
    }
    while(!pglobal->stop) {
        pthread_mutex_lock(&pglobal->in[input_number].db);
        // 等待输入通道发出数据更新的信号,唤醒
        pthread_cond_wait(&pglobal->in[input_number].db_update, &pglobal->in[input_number].db);
        ... ...
        // 从仓库中取出 一帧图像
        memcpy(frame, pglobal->in[input_number].buf, frame_size);
    
        pthread_mutex_unlock(&pglobal->in[input_number].db);
        ... ...
        sprintf(buffer, "Content-Type: image/jpeg\r\n" \
                "Content-Length: %d\r\n" \
                "X-Timestamp: %d.%06d\r\n" \
                "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
        // 发送报文,表明即将发送图像的大小 以及 时间戳
        if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break;
        
        if(write(context_fd->fd, frame, frame_size) < 0) break;
        // 发送报文,表明该帧结束
        sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
        if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break; 
    }   
    // 释放缓存
    free(frame);

若自己写对应的客户端:

  • 1.先发送一次请求字符串

    • “GET /?action=snapshot\n”
    • “GET /?action=stream\n”
    • “GET /?action=command\n”
    • … …
  • 2.再发送一次字符串,其中包含 用户名以及密码

    • "User-Agent: "
    • "Authorization: Basic "
      如果不需要密码功能,只需要发送任意长度 小于等于2字节的字符串,eg. “no”

如果 client 发送的请求为 “GET /?action=stream\n”

  • 3.接收一次字符串,为服务器发送的报文
  • 4.再接收一次报文,解析获取一帧图像的大小 以及 时间错
  • 5.接收 size 个字节的数据,转换为图像
  • 6.接收 报文尾消息
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Linux】mjpg-streamer 源码分析 的相关文章

  • 有没有办法只安装mysql客户端(Linux)? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 有没有不需要安装整个mysql db安装包的Linux mysql命令行工具 我想做的是从服务器 1 应用程序服务器 执行将在服务器 2
  • Linux mremap 不释放旧映射?

    我需要一种方法将页面从一个虚拟地址范围复制到另一个虚拟地址范围 而无需实际复制数据 范围很大 延迟很重要 mremap 可以做到这一点 但问题是它也会删除旧的映射 由于我需要在多线程环境中执行此操作 因此我需要旧映射能够同时使用 因此稍后当
  • SVN 不断提示我输入密码并拒绝缓存我的凭据

    环境 Eclipse Indigo Ubuntu 11 04 Subclipse 1 6 SVN 客户端 Subclipse RabbitVCS 我通过 svn ssh 连接 我的网址如下所示 svn ssh 我的名字 我的域名 路径 我可
  • 从sourceforge下载最新版本

    我正在尝试在 bash 脚本中从 Sourceforge 下载最新版本的graphicsmagick wget q https sourceforge net projects graphicsmagick files latest dow
  • 在哪里可以找到所有 C 标准库的源代码?

    我正在寻找所有 C 标准库的完整源代码 也就是说 我正在寻找 stdio h stdlib h string h math h 等的源代码 我想看看它们是如何创建的 我认为这取决于不同的平台 但 Linux 或 Windows 都会受到欢迎
  • 动态加载库和共享全局符号

    由于我在动态加载的库中观察到全局变量的一些奇怪行为 因此我编写了以下测试 首先我们需要一个静态链接库 头文件test hpp ifndef BASE HPP define BASE HPP include
  • OS X 对 /usr/local/lib 的权限被拒绝

    我正在寻找有关权限问题的任何建议 直觉 线索 答案 自从我切换到新的 Macbook Pro 以来 这个问题一直困扰着我 这就是困境 某些程序在安装期间复制 usr local lib 下的库 并且在运行这些程序时出现崩溃 我认为这与此文件
  • Linux shell 标题大小写

    我正在编写一个 shell 脚本并有一个如下所示的变量 something that is hyphenated 我需要在脚本中的各个点使用它 如下所示 something that is hyphenated somethingthati
  • CMake:使用其他平台的生成器。如何?

    如何使用 CMake 在 Linux 上生成 Visual Studio 项目文件 你不能 您必须在 Windows 上运行 CMake 才能为 Visual Studio 生成
  • 在 shell 脚本中将脚本目录更改为用户的 homedir

    在我的 bash 脚本中 我需要将当前目录更改为用户的主目录 如果我想更改为用户的foo主目录 从命令行我可以执行以下操作 cd foo 效果很好 但是当我从script它告诉我 bar sh line 4 cd foo No such f
  • 如何获取文件夹的大小,包括稀疏文件的表观大小? (du太慢了)

    我有一个包含很多KVM qcow2文件的文件夹 它们都是稀疏文件 现在我需要获取文件夹的总大小 qcow2 文件大小应计为表观大小 而不是实际大小 例如 图片 c9f38caf104b4d338cc1bbdd640dca89 qcow2 文
  • 查找当前打开的文件句柄数(不是 lsof )

    在 NIX系统上 有没有办法找出当前正在运行的进程中有多少个打开的文件句柄 我正在从正在运行的进程中寻找在 C 中使用的 API 或公式 在某些系统上 见下文 您可以在 proc pid fd 中对它们进行计数 如果不属于其中之一 请参阅下
  • 打破条件变量死锁

    我遇到这样的情况 线程 1 正在等待条件变量 A 该变量应该由线程 2 唤醒 现在线程 2 正在等待条件变量 B 该变量应该由线程 1 唤醒 在我使用的场景中条件变量 我无法避免这样的死锁情况 我检测到循环 死锁 并终止死锁参与者的线程之一
  • Bash 脚本错误 [重复]

    这个问题在这里已经有答案了 我想知道下面的脚本有什么错误 我收到错误为 command not foundh line 1 command not foundh line 2 其连续的 我试过添加 但现在工作请告诉我该怎么做 bin bas
  • pip 找不到满足要求的版本 django==2.2.1

    我刚刚将操作系统更改为 linux 并且想安装 django 但我无法安装最新版本的 django 我努力了 pip install django 但是它安装了 django 1 11 11 这不是我需要的 我还将我的 pip 升级到了 1
  • AMD OpenCL 在 Linux 上工作所需的最小必要文件子集是什么?

    我已经使用 buildroot 构建了 Linux 内核 我已将开源 amdgpu 驱动程序和所需的固件合并到其中 驱动程序很好 检测 GPU 模式设置运行良好 调整 小文本 的分辨率 启动后会显示命令行 现在我需要运行 OpenCL 程序
  • Pthread互斥锁由不同线程解锁

    一个天真的问题 我之前读到过 MUTEX 只能由锁定它的线程解锁 但我写了一个程序THREAD1锁定 mutexVar 并进入睡眠状态 然后THREAD2可以直接解锁mutexVar做一些操作并返回 gt 我知道每个人都说我为什么要这样做
  • 在 Windows 下使用 linux 实用程序的最佳方法是什么? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Linux 实用程序 如 sed awk 和其他 shell 脚本功能 非常棒 但当我在 Windows 上进行开发并且无法使用其中任何一
  • 串口读取未完成

    下面的函数用于在Linux下从串口读取数据 我在调试时可以读取完整的数据 但是当我启动程序时 读缓冲区似乎并不完整 我正确接收了一小部分数据 但缓冲区的其余部分完全正确zero 可能是什么问题呢 int8 t serial port ope
  • 如何每周日运行 crontab 作业

    我想弄清楚如何每周周日运行 crontab 作业 我认为以下应该可行 但我不确定我是否理解正确 下面的说法正确吗 5 8 6 这是 crontab 格式的解释 1 Entry Minute when the process will be

随机推荐

  • 【Android安全】Android app开发者证书和代码签名机制

    参考链接 xff1a 安卓证书相关验证机制 xff1a https duanqz github io 2017 09 01 Android Digital Signature xff08 部分内容不准确 xff09 关于META INF文件
  • 【Android安全】IDA 处理伪代码JUMPOUT指令(Undefine + Create Function)

    IDA 处理伪代码JUMPOUT指令 函数被IDA错误合并 IDA分析so时 xff0c 可能会遇到反编译结果不准确的情况 xff0c 如下 xff1a 这里的两个JUMPOUT其实解析有问题 xff0c 如下 xff1a 例如loc 18
  • 【Android安全】小米8刷机、救砖、root教程

    线刷 xff1a 通过计算机上的刷机软件把ROM 通过数据线传输 并安装到手机内存中 ROM包以tgz为后缀 卡刷 xff1a 把所需要的ROM下载或者复制到内存 SD卡根目录中 小米8救砖教程 xff08 线刷 xff09 按照https
  • 【Android抓包】Ubuntu mitmProxy配置

    Ubuntu 安装 mitmProxy 直接使用编译好的二进制包 参考 xff1a https cuiqingcai com 31053 html Linux E4 B8 8B E7 9A 84 E5 AE 89 E8 A3 85 直接下载
  • 【CSDN】查看自己的CSDN积分

    查看自己的CSDN积分 如何查看自己的CSDN博客积分 CSDN藏的比较深 xff0c 链接如下 xff1a https mp csdn net mp blog analysis article all CSDN博客积分与博客等级 参考 x
  • 【符号输入】打出撇号′

    打出撇号 撇号 xff08 apostrophe xff09 xff1a 搜狗输入法调成中文 xff0c 输入fen xff0c 第5个就是撇号
  • 【Android安全】xiaomi手机关闭adb安装应用时的确认提示

    xiaomi手机关闭adb安装应用时的确认提示 为了自动化测试 xff0c 需要关闭adb安装应用时的确认提示 需要分两步来关闭 xff1a 首先 xff0c 开发者选项 gt 启动MIUI优化 gt 关闭 xff08 第一步过后授权管理
  • 【python】报错UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte in position : illegal multibyte

    python读文件时报错 xff1a Traceback span class token punctuation span most recent call last span class token punctuation span F
  • STM32介绍

    目录 STM32 分类 STM8 和 STM32 分类 STM32 命名方法 STM32F103RCT6 寻找 IO 的功能 存储器映射 存储器 Block0 内部区域功能划分 存储器 Block1 内部区域功能划分 存储器 Block2
  • Putty串口打开无反应

    第一次使用putty的串口 xff0c 可能理所当然认为在Serial那里设置好参数 xff0c 然后点击Open就行了 但是显然不是 xff0c Putty的UI设计有问题 xff0c 不管你点击哪一个项 xff0c Open按钮始终都存
  • 【Android安全】r0capture使用

    r0capture使用 下载地址 xff1a https github com r0ysue r0capture 手机端启动frida server PC端安装frida client 命令 xff1a python r0capture s
  • MobaXterm或Xshell连接不上虚拟机ubuntu

    MobaXterm使用教程 xff1a MobaXterm官网下载 MobaXterm使用教程1 MobaXterm使用教程2 Xshell 使用教程 xff1a 恒源云远程登录Linux实例 包含下载地址和使用教程 Xshell使用教程
  • 591 标签验证器(模拟、栈匹配括号)

    1 问题描述 xff1a 给定一个表示代码片段的字符串 xff0c 你需要实现一个验证器来解析这段代码 xff0c 并返回它是否合法 合法的代码片段需要遵守以下的所有规则 xff1a 代码必须被合法的闭合标签包围 否则 xff0c 代码是无
  • 算法:最长公共子序列

    10 8算法实验报告 最长公共子序列 题目 输出两个字符串的最长公共子序列 要求1 不使用辅助数组 span class token comment 要求1 xff1a 不使用辅助数组 span span class token keywo
  • 呆呆和你谈谈入职CVTE一个月的感受

    呆呆和你谈谈入职CVTE一个月的感受 你盼世界 xff0c 我盼望你无bug Hello 大家好 xff01 我是霖呆呆 xff01 啊啊啊啊啊 至6 18日入职新公司CVTE已经一个多月了 xff0c 在 你盼世界 xff0c 我盼望你无
  • 编程就是调用API?如何成为造轮子的程序员

    是 xff0c 编程就是调用各种API 什么是API xff0c 就是别人把较复杂的代码封装成一个个函数 xff0c 你不用管函数怎么实现的 xff0c 直接用就好 从这个角度讲 xff0c 使用所有库 xff0c 框架 xff0c 模板
  • 【电赛】2019电子设计竞赛 纸张计数显示装置(F题)

    点击 Github项目地址 设计下载 内含 xff1a 电赛论文 程序设计 机械结构设计 硬件电路设计 综合测评相关设计 交互显示设计 设计详细说明 2019年全国大学生电子设计竞赛 纸张计数显示装置 xff08 F题 xff09 本科组
  • 【ARM裸板】LCD硬件原理、时序及初始化

    文章目录 1 LCD与OLED的区别2 LCD原理2 1 颜色如何确定 xff1f 2 2 LCD如何 行扫描 xff1f 2 3 如何跳到下一行进行 行扫描 xff1f 2 4 如何进行下一个 场扫描 xff1f 3 LCD时序4 LCD
  • 【电赛】2019电赛纸张计数显示装置Github仓库说明

    Github项目地址 设计下载 内含 xff1a 电赛论文 程序设计 机械结构设计 硬件电路设计 综合测评相关设计 交互显示设计 设计详细说明 纸张计数显示装置Github仓库说明 x1f604 个人主页 x1f57a 电赛论文 x1f4d
  • 【Linux】mjpg-streamer 源码分析

    文章目录 1 总体流程2 主进程的源码分析2 1 参数接收与解析2 2 获取参数2 3 调用输入函数2 3 1 程序手动中断信号2 3 2 strchr 函数2 3 3 strndup 函数2 3 4 分离参数 3 输入通道源码分析3 1