乐鑫 ESP-NOW

ESP-NOW 介绍

ESP-NOW 是乐鑫定义的一种无线通信协议,能够在无路由器的情况下直接、快速、低功耗地控制智能设备。它能够与 Wi-Fi 和 Bluetooth LE 共存,支持乐鑫 ESP8266、ESP32、ESP32-S 和 ESP32-C 等多系列 SoC。ESP-NOW 广泛应用于智能家电、远程控制和传感器等领域。

关于 ESP-NOW FAQ ,最大传输速率、应用场景、信道选择、配对限制等。

1、ESP-NOW 配对限制最多 20 个设备,是否有办法控制更多的设备?

使用广播包进行控制即可,目的地址包含在 payload 中,不受配对数量限制。仅需配置正确的广播地址即可。

2、ESP-NOW 最多可以控制多少个设备?

这取决于具体的通信方式:

  • 如使用单播包,支持同时最多配对并控制 20 个设备。
  • 如使用 ESP-NOW 加密模式,支持同时最多配对并控制 6 个设备。
  • 如使用广播包,仅需配置正确的广播地址即可。控制设备的数量理论上没有上限,但需考虑设备过多时的干扰问题。

3、为什么将 ESP-NOW 用户数据包的最大数据长度限制为 250 字节,这个数值可以修改吗?

  • 最长长度目前不能修改。因为 ESP-NOW 使用动作帧中的供应商特定元素字段传输数据,802.11 协议规定一个供应商特定元素中的长度字段只有 1 个字节 (0xff = 255),因此限制了正文部分 ESP-NOW 数据长度,最长为 250 字节。
  • 或者您可以使用 API esp_wifi_80211_tx() 发送数据,使用 sniffer 模式接收数据。这样同样可以实现只工作在 Wi-Fi 层并且不使用 TCP/IP 协议栈。

4、ESP-NOW 是否可以与 Wi-Fi 一起使用?

可以,但需要注意的是 ESP-NOW 的信道要和所连接的 AP 的信道相同。

5、ESP-NOW 设备间通信需要连接路由器吗?

ESP-NOW 的交互方式为直接从 设备到设备 进行通信,不需要通过路由器来转发数据。

6、使用 ESP-NOW 应用时,需要注意什么?

  • 连接 Wi-Fi 以后不能再切换信道,只能在当前 Wi-Fi 信道收发数据。
  • 如果设备进入 Modem-sleep 模式,将无法接受来自 ESP-NOW 的数据。

7、如果保证数据安全性?

ESP-NOW 可以通过 ECDH 和 AES128-CCM 来保护数据安全。

  • 使用 ECDH 和所有权证明 (PoP) 字符串授权会话、生成共享密钥
  • 使用 AES256-CTR 模式加密配置数据
  • 使用 AES128-CCM 模式加密 ESP-NOW 数据

ESP-NOW 数据帧

ESP-IDF 编程指南:ESP-NOW

ESP-NOW 是一种由乐鑫公司定义的 无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。

目前 ESP-NOW 支持两个版本:v1.0 和 v2.0:

  • v2.0 的设备支持的最大数据包长度为 1490 (ESP_NOW_MAX_DATA_LEN_V2) 字节;
  • v1.0 的设备支持的最大数据包长度为 250 (ESP_NOW_MAX_DATA_LEN) 字节。

v2.0 设备可以接收来自 v2.0 和 v1.0 设备的数据包。v1.0 设备只能接收来自 v1.0 设备的数据包。

当然,v1.0 设备也可以接收长度不超过 250 (ESP_NOW_MAX_IE_DATA_LEN) 的 v2.0 数据包,只是如果长度超过此值,就只接收前 250 (ESP_NOW_MAX_IE_DATA_LEN) 字节,或是直接丢弃数据包。

ESP-NOW 帧格式:

1
2
3
4
-----------------------------------------------------------------------------
| MAC 报头 | 分类代码 | 组织标识符 | 随机值 | 供应商特定内容 | FCS |
-----------------------------------------------------------------------------
24 字节 1 字节 3 字节 4 字节 7-x 字节 4 字节

帧数据包大小范围:43 字节 ~ 293 字节

供应商特定内容 帧格式:

1
2
3
4
5
--------------------------------------------------------------------------------------
| 元素 ID | 长度 | 组织标识符 | 类型 | 保留 | 版本 | 正文 |
--------------------------------------------------------------------------------------

1 字节 1 字节 3 字节 1 字节 7~4 比特| 3~0 比特 1 字节 0-250 字节

最大有效载荷:250 字节

配对设备:即将对方设备的MAC地址通过 esp_now_add_peer() 将其添加到配对设备列表中。配对设备的最大数量是 20,其中加密设备的数量不超过17,默认值是 7。

由于ESPNOW帧数据最大有效载荷:250 字节,乐鑫将帧数据有效载荷封装了应用层,其中应用层报头占用:20 字节,剩余 230 字节用于 body 部分。

应用层数据报:

1
2
3
4
-----------------------------------------------------------------------------
| 类型 | 版本 | 保留 | 数据大小 | 自定义帧头 | 目标MAC地址 | 源MAC地址 | 数据 |
-----------------------------------------------------------------------------
4 bit 2 bit 2 bit 1 Byte 6 Byte 6 字节 6 Byte 0~230 Byte

其中自定义帧头:

1
2
3
4
-----------------------------------------------------------------------------------------------------------
| 魔术数 | 信道 | 过滤相邻信道干扰位 | 过滤弱信号位 | 安全模式 | 保留 | 广播位 | 群组 | ACK位 | 重传次数 | TTL | RSSI |
-----------------------------------------------------------------------------------------------------------
2 byte 4 bit 1bit 1 bit 1 bit 4 bit 1 bit 1bit 1 bit 5 bit 5 bit 8 bit

ESP-NOW Control

对于 ESP-NOW Control Example 的解读。

ESP-NOW 根据数据流定义了两个角色:发起者(initiator)和响应者(responder)。同一个设备即可单独作为发起者,也可以单独作为响应者,也可以即作为发起者又作为响应者。

在物联网系统中,通常开关、传感器、液晶屏,而灯、插座作为响应者。

在 ESP-NOW Control Example 中,乐鑫又在 esp_now.h 的基础上封住了如下几个库:

1
2
3
4
5
6
7
- control
- debug
- espnow
- ota
- provisioning
- security
- utils

用户可以基于上述几个库开发上层应用,当然如果用户希望不想使用这几个库,也可以直接使用 esp_now.h 更加原始的库,然后用户自定义esp_now 应用层数据格式。

在 espnow/include/espnow.h 中封装了esp_now 应用层数据结构:

自定义类型帧头的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct espnow_frame_head_s {
uint16_t magic; // 2字节,魔术数字,用来表示数据包格式
uint8_t channel : 4; // 占用4位,表示使用的Wi-Fi信道
bool filter_adjacent_channel : 1; // 占用1位,是否过滤相邻信道的干扰
bool filter_weak_signal : 1; // 占用1位,是否过滤弱信号
bool security : 1; // 占用1位,是否启用安全模式
uint16_t : 4; // 占用4位,未命名填充位域,通常用于对齐或保留

/* Configure broadcast */
bool broadcast : 1; // 占用1位,是否为广播或单播
bool group : 1; // 占用1位,是否为广播的群组
bool ack : 1; // 占用1位,是否需要ACK确认
uint16_t retransmit_count : 5; // 占用5位,重传次数
uint8_t forward_ttl : 5; // 占用5位,转发TTL(Time To Live)
int8_t forward_rssi : 8; // 占用8位,转发时的RSSI(接收信号强度指示)
} __attribute__ ((packed)) espnow_frame_head_t;
1
2
3
4
5
6
7
8
9
10
typedef struct {
uint8_t type : 4; // 占用4位,表示数据包类型
uint8_t version : 2; // 占用2位,表示版本号
uint8_t : 2; // 未命名的2位,通常用户填充或保留
uint8_t size; // 1字节,表示payload有效载荷大小
espnow_frame_head_t frame_head; // 6字节,一个自定义类型的帧头
uint8_t dest_addr[6]; // 6字节,目标MAC地址
uint8_t src_addr[6]; // 6字节,源MAC地址
uint8_t payload[0]; // 可变长度数组,payload有效载荷从这里开始
} __attribute__((packed)) espnow_data_t;

__attribute__((packed)): 这是一个GCC编译器属性,告诉编译器不要对结构体成员进行默认的对齐优化,即按照每个成员声明的顺序紧密排列,以保证结构体在内存中的布局与协议要求一致,这对于网络通信非常重要。

那么对于 espnow_frame_head_t 结构体大小是 6 Byte,espnow_data_t 的结构体大小是 20 Bype。

注意:uint8_t payload[0] 在结构体中不占用,通过sizeof(espnow_data_t) 得出只有 20 Byte

数据包类型定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief Divide ESP-NOW data into multiple pipes
*/
typedef enum {
ESPNOW_DATA_TYPE_ACK, /**< For reliable data transmission */
ESPNOW_DATA_TYPE_FORWARD, /**< Set to forward packets */
ESPNOW_DATA_TYPE_GROUP, /**< Send a packet that sets the group type */
ESPNOW_DATA_TYPE_PROV, /**< Network configuration packet */
ESPNOW_DATA_TYPE_CONTROL_BIND, /**< Binding or unbinding packet */
ESPNOW_DATA_TYPE_CONTROL_DATA, /**< Control data packet */
ESPNOW_DATA_TYPE_OTA_STATUS, /**< Status packet for rapid upgrade of batch Device */
ESPNOW_DATA_TYPE_OTA_DATA, /**< Data packet for rapid upgrade of batch Device */
ESPNOW_DATA_TYPE_DEBUG_LOG, /**< Equipment debugging log packet */
ESPNOW_DATA_TYPE_DEBUG_COMMAND, /**< Equipment debugging command packet */
ESPNOW_DATA_TYPE_DATA, /**< User-defined use */
ESPNOW_DATA_TYPE_SECURITY_STATUS,/**< Security status packet */
ESPNOW_DATA_TYPE_SECURITY, /**< Security handshake packet */
ESPNOW_DATA_TYPE_SECURITY_DATA, /**< Security packet */
ESPNOW_DATA_TYPE_RESERVED, /**< Reserved for other function */
ESPNOW_DATA_TYPE_MAX,
} espnow_data_type_t;

在 control/include/espnow_ctrl.h 中对于 initiator 和 responder 的控制数据封装了数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* espnow_attribute_t 结构:控制属性,有light、button、battery */
typedef enum {
ESPNOW_ATTRIBUTE_BASE = 0x0000,
ESPNOW_ATTRIBUTE_POWER = 0x0001,
ESPNOW_ATTRIBUTE_POWER_ADD = 0x0002,

ESPNOW_ATTRIBUTE_ATTRIBUTE = 0x0003,

/**< light */
ESPNOW_ATTRIBUTE_LIGHT_BASE = 0x0100,
ESPNOW_ATTRIBUTE_BRIGHTNESS = 0x0101,
ESPNOW_ATTRIBUTE_BRIGHTNESS_ADD = 0x0102,
ESPNOW_ATTRIBUTE_HUE = 0x0103,
ESPNOW_ATTRIBUTE_HUE_ADD = 0x0104,
ESPNOW_ATTRIBUTE_SATURATION = 0x0105,
ESPNOW_ATTRIBUTE_SATURATION_ADD = 0x0106,
ESPNOW_ATTRIBUTE_WARM = 0x0107,
ESPNOW_ATTRIBUTE_WARM_ADD = 0x0108,
ESPNOW_ATTRIBUTE_COLD = 0x0109,
ESPNOW_ATTRIBUTE_COLD_ADD = 0x010a,
ESPNOW_ATTRIBUTE_RED = 0x010b,
ESPNOW_ATTRIBUTE_RED_ADD = 0x010c,
ESPNOW_ATTRIBUTE_GREEN = 0x010d,
ESPNOW_ATTRIBUTE_GREEN_ADD = 0x010e,
ESPNOW_ATTRIBUTE_BLUE = 0x010f,
ESPNOW_ATTRIBUTE_BLUE_ADD = 0x0110,
ESPNOW_ATTRIBUTE_MODE = 0x0111,
ESPNOW_ATTRIBUTE_MODE_ADD = 0x0112,

/**< button */
ESPNOW_ATTRIBUTE_BUTTON_BASE = 0x0200,
ESPNOW_ATTRIBUTE_KEY_1 = 0x0201,
ESPNOW_ATTRIBUTE_KEY_2 = 0x0202,
ESPNOW_ATTRIBUTE_KEY_3 = 0x0203,
ESPNOW_ATTRIBUTE_KEY_4 = 0x0204,
ESPNOW_ATTRIBUTE_KEY_5 = 0x0205,
ESPNOW_ATTRIBUTE_KEY_6 = 0x0206,
ESPNOW_ATTRIBUTE_KEY_7 = 0x0207,
ESPNOW_ATTRIBUTE_KEY_8 = 0x0208,
ESPNOW_ATTRIBUTE_KEY_9 = 0x0209,
ESPNOW_ATTRIBUTE_KEY_10 = 0x0210,

/**< battery */
ESPNOW_ATTRIBUTE_BATTERY_BASE = 0x0300,
ESPNOW_ATTRIBUTE_STATUS_LOW_BATTERY = 0x0301,
ESPNOW_ATTRIBUTE_BATTERY_LEVEL = 0x0302,
ESPNOW_ATTRIBUTE_CHARGING_STATE = 0x0303,
} espnow_attribute_t;


typedef struct espnow_ctrl_data_s {
#ifdef CONFIG_ESPNOW_CONTROL_AUTO_CHANNEL_SENDING
espnow_frame_head_t frame_head; // 6字节
#endif
espnow_attribute_t initiator_attribute; // 2字节 /**< Initiator's attribute */
espnow_attribute_t responder_attribute; // 2字节 /**< Responder's attribute */
union {
bool responder_value_b;
int responder_value_i;
float responder_value_f;
struct {
uint32_t responder_value_s_flag : 24;
uint8_t responder_value_s_size;
}; // 联合体根据实际使用时的计算大小
};

char responder_value_s[0];
} espnow_ctrl_data_t;

在 espnow/include/espnow.h 中对于ESP_NOW 事件也进行了定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief Enumerated list of control event id
*/
#define ESP_EVENT_ESPNOW_CTRL_BIND (ESP_EVENT_ESPNOW_CTRL_BASE + 0)
#define ESP_EVENT_ESPNOW_CTRL_UNBIND (ESP_EVENT_ESPNOW_CTRL_BASE + 1)
#define ESP_EVENT_ESPNOW_CTRL_BIND_ERROR (ESP_EVENT_ESPNOW_CTRL_BASE + 2)

/**
* @brief Declaration of the task events family
*/
#define ESP_EVENT_DECLARE_BASE(id) extern esp_event_base_t const id
ESP_EVENT_DECLARE_BASE(ESP_EVENT_ESPNOW);

#define ESP_EVENT_ESPNOW_PROV_BASE 0x100
#define ESP_EVENT_ESPNOW_CTRL_BASE 0x200
#define ESP_EVENT_ESPNOW_OTA_BASE 0x300
#define ESP_EVENT_ESPNOW_DEBUG_BASE 0x400
#define ESP_EVENT_ESPNOW_RESERVED_BASE 0x500

ESP-NOW Provisioning

在配网中也要对ESP32设备分为两种角色:发起者(initiator)和 响应者(responder)

  • 发起者

    1、持续扫描响应者发出 ESP-NOW 的 provision beacon(信标)

    2、当接收响应者发出的 provision beacon后,发送 “设备类型配置帧”,用于向响应者请求 Wi-Fi凭证(即AP的SSID和密码)

    3、如果接收到响应者发出的 provision 帧后,则从数据帧中获取 Wi-Fi 凭证,并连接到目标AP

  • 响应者

    1、在 30 秒内,每 100 毫秒广播一次 provision beacon

    2、如果接收到发起者发出的 “设备类型配置帧” 后,则发出包含 Wi-Fi 凭证的 Wi-Fi 类型配置帧进行响应

在 provisioning/include/espnow_prov.h 定义了 esp_now 配网数据包类型:

1
2
3
4
5
6
typedef enum {
ESPNOW_PROV_TYPE_BEACON,
ESPNOW_PROV_TYPE_DEVICE,
ESPNOW_PROV_TYPE_WIFI,
} espnow_prov_type_t;

当然配网数据包结构也要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define ESPNOW_PACKED_STRUCT                __attribute__ ((packed))

typedef enum {
ESPNOW_PROV_AUTH_INVALID, /**< Invalid mode */
ESPNOW_PROV_AUTH_PRODUCT, /**< Product authentication */
ESPNOW_PROV_AUTH_DEVICE, /**< Device authentication */
ESPNOW_PROV_AUTH_CERT, /**< Certificate authentication */
} espnow_prov_auth_mode_t; // 4 Byte

typedef struct espnow_prov_initiator_s {
char product_id[16]; /**< Product ID */
char device_name[16]; /**< Device name */
espnow_prov_auth_mode_t auth_mode; /**< Authentication mode of provisioning */
union {
char device_secret[32]; /**< Device security key */
char product_secret[32]; /**< Product security key */
char cert_secret[32]; /**< Certify security key */
};
uint8_t custom_size; /**< Customer data size */
uint8_t custom_data[0]; /**< Customer data */
} ESPNOW_PACKED_STRUCT espnow_prov_initiator_t; // 69 Byte

typedef struct espnow_prov_responder_s {
char product_id[16]; /**< Product ID */
char device_name[16]; /**< Device name */
} ESPNOW_PACKED_STRUCT espnow_prov_responder_t; // 32 Byte

typedef enum {
WIFI_MODE_NULL = 0, /**< null mode */
WIFI_MODE_STA, /**< WiFi station mode */
WIFI_MODE_AP, /**< WiFi soft-AP mode */
WIFI_MODE_APSTA, /**< WiFi station + soft-AP mode */
WIFI_MODE_NAN, /**< WiFi NAN mode */
WIFI_MODE_MAX
} wifi_mode_t; // 4 Byte

typedef struct espnow_prov_wifi_s {
wifi_mode_t mode; /**< WiFi mode */
union {
wifi_ap_config_t ap; /**< Configuration of AP */
wifi_sta_config_t sta; /**< Configuration of STA */
};
char token[32]; /**< Token of the WiFi configuration */
uint8_t custom_size; /**< Customer data size */
uint8_t custom_data[0]; /**< Customer data */
} ESPNOW_PACKED_STRUCT espnow_prov_wifi_t; // 217 Byte

typedef struct {
uint8_t type; // 1 Byte
union {
espnow_prov_initiator_t initiator_info; // 69 Byte
espnow_prov_responder_t responder_info; // 32 Byte
espnow_prov_wifi_t wifi_config; // 217 Byte
};
} __attribute__((packed)) espnow_prov_data_t;

responder (dc:da:0c:d2:4f:74)创建定时器,每100ms发送 ESPNOW_DATA_TYPE_PROV 广播数据,并注册回调函数用来接收 ESPNOW_DATA_TYPE_PROV 类型的数据。

1
2
# responder 发送广播数据
data(33): 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

initiator(f0:9e:9e:99:12:60) 持续扫描 ESPNOW_DATA_TYPE_PROV 类型的数据,接收到 ESPNOW_DATA_TYPE_PROV 类型数据后,接着发送 ESPNOW_DATA_TYPE_PROV 的单播数据,并注册回调函数接收 ESPNOW_DATA_TYPE_PROV 类型数据:

1
2
# initiator 接收广播数据
data(33): 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
1
2
# initiator 发送单播数据
data(218): 01 69 6e 69 74 69 61 74 6f 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60 00 06 00 23 21 8a 55 11 88 02 00 ff ff ff ff ff ff dc da 0c d2 4f 74 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc ab ca 3f eb 00 00 00 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 02 85 00 70 ec 00 00 00 00 01 01 42 d6 10 ac ca 3f 43 53 01 00 14 8d c9 3f e0 ab

responder 接收到 ESPNOW_DATA_TYPE_PROV 类型的数据后,接着发送 ESPNOW_DATA_TYPE_PROV 类型的单播数据:

1
2
# responder 接收单播数据
data(218): 01 69 6e 69 74 69 61 74 6f 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60 00 06 00 23 21 8a 55 11 88 02 00 ff ff ff ff ff ff dc da 0c d2 4f 74 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc ab ca 3f eb 00 00 00 00 72 65 73 70 6f 6e 64 65 72 5f 74 65 73 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 02 85 00 70 ec 00 00 00 00 01 01 42 d6 10 ac ca 3f 43 53 01 00 14 8d c9 3f e0 ab
1
2
# responder 发送单播数据
data(218): 02 00 00 00 00 43 55 42 45 43 5f 41 50 5f 32 2e 34 47 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 63 75 62 65 63 32 30 31 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

initiator 接收到 ESPNOW_DATA_TYPE_PROV 类型数据后,接着去连接目标AP

1
data(218): 02 00 00 00 00 43 55 42 45 43 5f 41 50 5f 32 2e 34 47 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 63 75 62 65 63 32 30 31 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00