通过stm32板卡的网络功能,实现通过网页浏览器直接访问设备,对设备的信息进行查询和设置。

此方法常用于路由器,可以直接省去上位机软件下载。简单轻便的访问设备。

开发步骤如下:

第一步:lwip加入http插件

在cubemx工程中的lwip三方库勾选http后进行生成

生成完成后可以在代码的该工程下看到以下源码已经被添加进来

代码在lwip初始化后添加httpd_init()即可

makefsdata工具

除了http源码,还有一个工具:makefsdata也很重要,该工具可以将你编写好的html代码转为文件系统的格式(通过内部flash存储的静态数组)。相关工具在lwip官网下载

使用方法:把编写的.html或.shtml文件放入fs目录下,执行makefsdata.exe程序。会生成对应的fsdata.c文件。把fsdata.c文件拷贝到http目录下进行编译。

#define file_NULL (struct fsdata_file *) NULL


static const unsigned int dummy_align__index_shtml = 0;
static const unsigned char data__index_shtml[] = {
/* /index.shtml (13 chars) */
0x2f,0x69,0x6e,0x64,0x65,0x78,0x2e,0x73,0x68,0x74,0x6d,0x6c,0x00,0x00,0x00,0x00,

/* HTTP header */
/* "HTTP/1.0 200 OK
" (17 bytes) */
0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x30,0x20,0x32,0x30,0x30,0x20,0x4f,0x4b,0x0d,
0x0a,
/* "Server: lwIP/1.3.1 (http://savannah.nongnu.org/projects/lwip)
" (63 bytes) */
0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x6c,0x77,0x49,0x50,0x2f,0x31,0x2e,0x33,
0x2e,0x31,0x20,0x28,0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x73,0x61,0x76,0x61,0x6e,
0x6e,0x61,0x68,0x2e,0x6e,0x6f,0x6e,0x67,0x6e,0x75,0x2e,0x6f,0x72,0x67,0x2f,0x70,
0x72,0x6f,0x6a,0x65,0x63,0x74,0x73,0x2f,0x6c,0x77,0x69,0x70,0x29,0x0d,0x0a,
/* "Content-type: text/html
Expires: Fri, 10 Apr 2008 14:00:00 GMT
Pragma: no-cache

" (85 bytes) */
0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x74,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,
0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x0d,0x0a,0x45,0x78,0x70,0x69,0x72,0x65,0x73,
0x3a,0x20,0x46,0x72,0x69,0x2c,0x20,0x31,0x30,0x20,0x41,0x70,0x72,0x20,0x32,0x30,
0x30,0x38,0x20,0x31,0x34,0x3a,0x30,0x30,0x3a,0x30,0x30,0x20,0x47,0x4d,0x54,0x0d,
0x0a,0x50,0x72,0x61,0x67,0x6d,0x61,0x3a,0x20,0x6e,0x6f,0x2d,0x63,0x61,0x63,0x68,
0x65,0x0d,0x0a,0x0d,0x0a,
/* raw file data (6767 bytes) */
0x3c,0x21,0x44,0x4f,0x43,0x54,0x59,0x50,0x45,0x20,0x68,0x74,0x6d,0x6c,0x3e,0x0d,
0x0a,0x3c,0x68,0x74,0x6d,0x6c,0x3e,0x0d,0x0a,0x3c,0x68,0x65,0x61,0x64,0x3e,0x0d,
0x0a,0x3c,0x6d,0x65,0x74,0x61,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x22,}

const struct fsdata_file file__index_shtml[] = { {
file_NULL,
data__index_shtml,
data__index_shtml + 16,
sizeof(data__index_shtml) - 16,
1,
}};

#define FS_ROOT file__index_shtml
#define FS_NUMFILES 1

file__index_shtml的第一个变量指向下一个文件,结构体如下。

lwip自带的fs.c(文件系统源码)通过链表遍历所有文件数据,比较文件名来找到对应文件。

如果想要使用其他文件系统和外部flash,需要对这些代码进行更换。

struct fsdata_file {
  const struct fsdata_file *next;
  const unsigned char *name;
  const unsigned char *data;
  int len;
  u8_t flags;
#if HTTPD_PRECALCULATED_CHECKSUM
  u16_t chksum_count;
  const struct fsdata_chksum *chksum;
#endif /* HTTPD_PRECALCULATED_CHECKSUM */
};

支持协议

lwip的http只支持ssi和cgi两种,如果想要使用传输数据更多,实时性更强的web socket。需要对http进行改造,此处就不再介绍。

SSI:是一种类似于 ASP 的基于服务器的网页制作技术。大多数的 WEB 服务器等均支持 SSI 命令。将内容发送到浏览器之前,可以使用“服务器端包含 (SSI)”指令将文本、图形或应用程序信息包含到网页中。因为包含 SSI 指令的文件要求特殊处理,所以必须为所有 SSI 文件赋予 SSI 文件扩展名。默认扩展名是 .stm、.shtm 和 .shtml。
可以理解为SSI接口只能进行简单的显示,不能进行参数设置。

CGI: 规范允许 Web 服务器执行外部程序,并将它们的输出发送给Web 浏览器,CGI 在物理上是一段程序,运行在服务器上,提供同客户端 HTML 页面的接口。

可以理解为网页下发http://192.168.5.10/led=on到服务器,服务器会获取后缀后的led=on对应的意思。然后服务器执行对应已经预设好了的程序。返回结果到网页上显示。

测试过程中通过F12查看网页调试工具中的network可以看到请求包与服务器回包。更具体的数据(包含tcp包头,mac数据等)建议使用wireshark进行抓包。

LWIP宏配置

#define LWIP_HTTPD_SSI 1 //使能SSI接口
#define LWIP_HTTPD_CGI 1    //使能CGI接口
#define LWIP_HTTPD_DYNAMIC_HEADERS 1
#define HTTP_IS_DATA_VOLATILE(hs) TCP_WRITE_FLAG_COPY 

#define MEM_SIZE 30*1024
// 增加TCP发送缓冲区(例如:16KB),确保能装下整个页面
#define TCP_SND_BUF                       4096*2
//
//// 增加TCP接收窗口,与发送端匹配
#define TCP_WND                           4096
//// 调整TCP报文段数量,公式一般为 4 * TCP_SND_BUF/TCP_MSS
//#define TCP_MSS                            1460//TCP每包的数据量
#define TCP_SND_QUEUELEN        (4 * TCP_SND_BUF/TCP_MSS)
#define MEMP_NUM_TCP_SEG (TCP_SND_QUEUELEN+1)

打印调试方法

LWIP源码在函数调用过程中已经添加了很多打印帮助定位问题与分析。

只需要设置 #define LWIP_DEBUG 1 进行,同时还有很多分模块宏对不同功能模块的打印进行区分。如

#define LWIP_DEBUG 1
#define HTTPD_DEBUG LWIP_DBG_ON
#define LWIP_DBG_TYPES_ON (LWIP_DBG_ON|LWIP_DBG_TRACE|LWIP_DBG_STATE|LWIP_DBG_FRESH)
#define LWIP_DBG_MIN_LEVEL LWIP_DBG_LEVEL_ALL

//关闭以太网打印
#define ETHARP_DEBUG LWIP_DBG_OFF
#define ETH_DEBUG LWIP_DBG_OFF
#define NETIF_DEBUG LWIP_DBG_OFF
#define ETHERNET_DEBUG LWIP_DBG_OFF

SSI接口

初始化

#define NUM_CONFIG_SSI_TAGS 6

static const char *ppcTAGs[] = {
    "V_OPEN",     // 阀门开度 (原VALVE_OPENING)
    "FLOW",       // 流量 (原FLOW_RATE)
    "PRESS",      // 压力 (原PRESSURE)
    "TEMP",       // 温度 (原TEMPERATURE)
    "V_STAT",     // 阀门状态 (原VALVE_STATUS)
    "RUN_T"       // 运行时间 (原RUNNING_TIME)
};

// SSI处理器函数
u16_t SSIHandler(int iIndex, char *pcInsert, int iInsertLen) {
    static unsigned char i = 1;
    int valveOpening = 1;        // 读取阀门开度
    float flowRate = 2.0;        // 读取流量
    float pressure = 3.0;        // 读取压力
    float temperature = 4.0;     // 读取温度
    int valveStatus = 5;         // 阀门状态
    int runningTime = i++;       // 运行时间
    
    if(i > 6000){
        i = 1;
    }
    
    // 根据索引返回对应的数据
    switch(iIndex) {
        case 0: // VALVE_OPENING
            return snprintf(pcInsert, iInsertLen, "%d", valveOpening);
        case 1: // FLOW_RATE
            return snprintf(pcInsert, iInsertLen, "%.2f", flowRate);
        case 2: // PRESSURE
            return snprintf(pcInsert, iInsertLen, "%.2f", pressure);
        case 3: // TEMPERATURE
            return snprintf(pcInsert, iInsertLen, "%.1f", temperature);
        case 4: // VALVE_STATUS
            return snprintf(pcInsert, iInsertLen, "%d", valveStatus);
        case 5: // RUNNING_TIME
            return snprintf(pcInsert, iInsertLen, "%d", runningTime);
        default:
            return 0;
    }
}


void http_Init(void){
 
	httpd_init();
	printf("start http server\r\n");
	http_set_ssi_handler(SSIHandler, ppcTAGs, NUM_CONFIG_SSI_TAGS); 
}

调用关系:

http_send被调用时会触发Http_send_data_ssi(比如当web get html时候,http协议栈会把html文件发送,在发送前http_send_data_ssi会解析html中有<!-- >的部分,替换成对应的数值进行上报)

/**
 * Try to send more data on this pcb.
 *
 * @param pcb the pcb to send data
 * @param hs connection state
 */
static u8_t
http_send(struct altcp_pcb *pcb, struct http_state *hs)
{
  u8_t data_to_send = HTTP_NO_DATA_TO_SEND;

  LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("http_send: pcb=%p hs=%p left=%d\n", (void *)pcb,
              (void *)hs, hs != NULL ? (int)hs->left : 0));

#if LWIP_HTTPD_SUPPORT_POST && LWIP_HTTPD_POST_MANUAL_WND
  if (hs->unrecved_bytes != 0) {
    return 0;
  }
#endif /* LWIP_HTTPD_SUPPORT_POST && LWIP_HTTPD_POST_MANUAL_WND */

  /* If we were passed a NULL state structure pointer, ignore the call. */
  if (hs == NULL) {
    return 0;
  }

#if LWIP_HTTPD_FS_ASYNC_READ
  /* Check if we are allowed to read from this file.
     (e.g. SSI might want to delay sending until data is available) */
  if (!fs_is_file_ready(hs->handle, http_continue, hs)) {
    return 0;
  }
#endif /* LWIP_HTTPD_FS_ASYNC_READ */

#if LWIP_HTTPD_DYNAMIC_HEADERS
  /* Do we have any more header data to send for this file? */
  if (hs->hdr_index < NUM_FILE_HDR_STRINGS) {
    data_to_send = http_send_headers(pcb, hs);
    if ((data_to_send == HTTP_DATA_TO_SEND_FREED) ||
        ((data_to_send != HTTP_DATA_TO_SEND_CONTINUE) &&
         (hs->hdr_index < NUM_FILE_HDR_STRINGS))) {
      return data_to_send;
    }
  }
#endif /* LWIP_HTTPD_DYNAMIC_HEADERS */

  /* Have we run out of file data to send? If so, we need to read the next
   * block from the file. */
  if (hs->left == 0) {
    if (!http_check_eof(pcb, hs)) {
      return 0;
    }
  }

#if LWIP_HTTPD_SSI
  if (hs->ssi) {
    data_to_send = http_send_data_ssi(pcb, hs);
  } else
#endif /* LWIP_HTTPD_SSI */
  {
    data_to_send = http_send_data_nonssi(pcb, hs);
  }

  if ((hs->left == 0) && (fs_bytes_left(hs->handle) <= 0)) {
    /* We reached the end of the file so this request is done.
     * This adds the FIN flag right into the last data segment. */
    LWIP_DEBUGF(HTTPD_DEBUG, ("End of file.\n"));
    http_eof(pcb, hs);
    return 0;
  }
  LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("send_data end.\n"));
  return data_to_send;
}


htttp_send_data_ssi解析index.html文件中的ssi标签<!--#vale_opening-->,然后使用定义的ssihandler把对应数据插入html文件中发给客户端
ssi->tag_name和注册时额http_tags[tags]一致,执行对应的函数
把处理后的返回值放到ssi->tag_insert中。

注意:
LWIP_HTTPD_MAX_TAG_NAME_LEN 需要调整为大于ppcTAGS数组的长度,不然会匹配不上

CGI接口

初始化

static const tCGI ppcURLs[] = /* cgi程序 */
{
    {"/leds.cgi", LEDS_CGI_Handler},//网页发来的name和对应的处理函数
    // {"/valve_data.cgi", VALVE_DATA_CGI_Handler},
};
#define NUM_CONFIG_CGI_URIS 1 //cgi的数量
http_set_cgi_handlers(ppcURLs, NUM_CONFIG_CGI_URIS);  /* 配置CGI句柄 */

调用关系:

//把句柄赋值给httpd_cgis 
void http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
{
  LWIP_ASSERT("no cgis given", cgis != NULL);
  LWIP_ASSERT("invalid number of handlers", num_handlers > 0);

  httpd_cgis = cgis;
  httpd_num_cgis = num_handlers;
}

lwip收到http协议,http_recv调用http_parse_request,http_parse_request进行安全和长度判断后解析出URI指令,调用http_find_file函数。

/** Try to find the file specified by uri and, if found, initialize hs
 * accordingly.
 *
 * @param hs the connection state
 * @param uri the HTTP header URI
 * @param is_09 1 if the request is HTTP/0.9 (no HTTP headers in response)
 * @return ERR_OK if file was found and hs has been initialized correctly
 *         another err_t otherwise
 */
static err_t
http_find_file(struct http_state *hs, const char *uri, int is_09)
{
  size_t loop;
  struct fs_file *file = NULL;
  char *params = NULL;
  err_t err;
#if LWIP_HTTPD_CGI
  int i;
#endif /* LWIP_HTTPD_CGI */
#if !LWIP_HTTPD_SSI
  const
#endif /* !LWIP_HTTPD_SSI */
  /* By default, assume we will not be processing server-side-includes tags */
  u8_t tag_check = 0;
  printf("%s\r\n",uri);
  /* Have we been asked for the default file (in root or a directory) ? */
#if LWIP_HTTPD_MAX_REQUEST_URI_LEN
  size_t uri_len = strlen(uri);
  if ((uri_len > 0) && (uri[uri_len - 1] == '/') &&
      ((uri != http_uri_buf) || (uri_len == 1))) {
    size_t copy_len = LWIP_MIN(sizeof(http_uri_buf) - 1, uri_len - 1);
    if (copy_len > 0) {
      MEMCPY(http_uri_buf, uri, copy_len);
      http_uri_buf[copy_len] = 0;
    }
#else /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
  if ((uri[0] == '/') &&  (uri[1] == 0)) {
#endif /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
    /* Try each of the configured default filenames until we find one
       that exists. */
    for (loop = 0; loop < NUM_DEFAULT_FILENAMES; loop++) {
      const char *file_name;
#if LWIP_HTTPD_MAX_REQUEST_URI_LEN
      if (copy_len > 0) {
        size_t len_left = sizeof(http_uri_buf) - copy_len - 1;
        if (len_left > 0) {
          size_t name_len = strlen(httpd_default_filenames[loop].name);
          size_t name_copy_len = LWIP_MIN(len_left, name_len);
          MEMCPY(&http_uri_buf[copy_len], httpd_default_filenames[loop].name, name_copy_len);
          http_uri_buf[copy_len + name_copy_len] = 0;
        }
        file_name = http_uri_buf;
      } else
#endif /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
      {
        file_name = httpd_default_filenames[loop].name;
      }
      LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Looking for %s...\n", file_name));
      printf("%s filename :%s\r\n",__func__,file_name);
      err = fs_open(&hs->file_handle, file_name);
      if (err == ERR_OK) {
        uri = file_name;
        file = &hs->file_handle;
        LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Opened.\n"));
        printf("file name opne ok\r\n");
#if LWIP_HTTPD_SSI
        tag_check = httpd_default_filenames[loop].shtml;
#endif /* LWIP_HTTPD_SSI */
        break;
      }
    }
  }
  if (file == NULL) {
    /* No - we've been asked for a specific file. */
    /* First, isolate the base URI (without any parameters) */
    params = (char *)strchr(uri, '?');
    if (params != NULL) {
      /* URI contains parameters. NULL-terminate the base URI */
      *params = '\0';
      params++;
    }

#if LWIP_HTTPD_CGI
    http_cgi_paramcount = -1;
    /* Does the base URI we have isolated correspond to a CGI handler? */
    if (httpd_num_cgis && httpd_cgis) {
      for (i = 0; i < httpd_num_cgis; i++) {
        if (strcmp(uri, httpd_cgis[i].pcCGIName) == 0) {
          /*
           * We found a CGI that handles this URI so extract the
           * parameters and call the handler.
           */
          http_cgi_paramcount = extract_uri_parameters(hs, params);
          uri = httpd_cgis[i].pfnCGIHandler(i, http_cgi_paramcount, hs->params,
                                         hs->param_vals);
          break;
        }
      }
    }
#endif /* LWIP_HTTPD_CGI */

    LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Opening %s\n", uri));

    err = fs_open(&hs->file_handle, uri);
    if (err == ERR_OK) {
      file = &hs->file_handle;
    } else {
      file = http_get_404_file(hs, &uri);
    }
#if LWIP_HTTPD_SSI
    if (file != NULL) {
      if (file->flags & FS_FILE_FLAGS_SSI) {
        tag_check = 1;
      } else {
#if LWIP_HTTPD_SSI_BY_FILE_EXTENSION
        tag_check = http_uri_is_ssi(file, uri);
#endif /* LWIP_HTTPD_SSI_BY_FILE_EXTENSION */
      }
    }
#endif /* LWIP_HTTPD_SSI */
  }
  if (file == NULL) {
    /* None of the default filenames exist so send back a 404 page */
    file = http_get_404_file(hs, &uri);
  }
  return http_init_file(hs, file, is_09, uri, tag_check, params);
}

http_find_file函数有三个主要功能:

根据uri返回对应的html文件给web,当uri = /时,会从fsdata.c的文件系统中选出与httpd_default_filenames数组中文件名相同的html文件返回给web。这个html文件显示的内容也就是你网页上可以浏览到的初始内容。

httpd_default_filenames不做更改时默认名字有

static const default_filename httpd_default_filenames[] = {
  {"/index.shtml", 1 },
  {"/index.ssi",   1 },
  {"/index.shtm",  1 },
  {"/index.html",  0 },
  {"/index.htm",   0 }
};

所以fsdata.c中的Html文件名也最好叫index.html。如果你要用新的名字,把新名字加 在这个数组中。

另外两个功能就是进行ssi和cgi的处理

代码示例参照:https://blog.csdn.net/qq_44712722/article/details/148290192

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐