1 // Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #define LOG_TAG "bt_osi_config"
16 #include "esp_system.h"
17 #include "nvs_flash.h"
18 #include "nvs.h"
19
20 #include <ctype.h>
21 #include <errno.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25
26 #include "bt_common.h"
27 #include "osi/allocator.h"
28 #include "osi/config.h"
29 #include "osi/list.h"
30
31 #define CONFIG_FILE_MAX_SIZE (1536)//1.5k
32 #define CONFIG_FILE_DEFAULE_LENGTH (2048)
33 #define CONFIG_KEY "bt_cfg_key"
34 typedef struct {
35 char *key;
36 char *value;
37 } entry_t;
38
39 typedef struct {
40 char *name;
41 list_t *entries;
42 } section_t;
43
44 struct config_t {
45 list_t *sections;
46 };
47
48 // Empty definition; this type is aliased to list_node_t.
49 struct config_section_iter_t {};
50
51 static void config_parse(nvs_handle_t fp, config_t *config);
52
53 static section_t *section_new(const char *name);
54 static void section_free(void *ptr);
55 static section_t *section_find(const config_t *config, const char *section);
56
57 static entry_t *entry_new(const char *key, const char *value);
58 static void entry_free(void *ptr);
59 static entry_t *entry_find(const config_t *config, const char *section, const char *key);
60
config_new_empty(void)61 config_t *config_new_empty(void)
62 {
63 config_t *config = osi_calloc(sizeof(config_t));
64 if (!config) {
65 OSI_TRACE_ERROR("%s unable to allocate memory for config_t.\n", __func__);
66 goto error;
67 }
68
69 config->sections = list_new(section_free);
70 if (!config->sections) {
71 OSI_TRACE_ERROR("%s unable to allocate list for sections.\n", __func__);
72 goto error;
73 }
74
75 return config;
76
77 error:;
78 config_free(config);
79 return NULL;
80 }
81
config_new(const char * filename)82 config_t *config_new(const char *filename)
83 {
84 assert(filename != NULL);
85
86 config_t *config = config_new_empty();
87 if (!config) {
88 return NULL;
89 }
90
91 esp_err_t err;
92 nvs_handle_t fp;
93 err = nvs_open(filename, NVS_READWRITE, &fp);
94 if (err != ESP_OK) {
95 if (err == ESP_ERR_NVS_NOT_INITIALIZED) {
96 OSI_TRACE_ERROR("%s: NVS not initialized. "
97 "Call nvs_flash_init before initializing bluetooth.", __func__);
98 } else {
99 OSI_TRACE_ERROR("%s unable to open NVS namespace '%s'\n", __func__, filename);
100 }
101 config_free(config);
102 return NULL;
103 }
104
105 config_parse(fp, config);
106 nvs_close(fp);
107 return config;
108 }
109
config_free(config_t * config)110 void config_free(config_t *config)
111 {
112 if (!config) {
113 return;
114 }
115
116 list_free(config->sections);
117 osi_free(config);
118 }
119
config_has_section(const config_t * config,const char * section)120 bool config_has_section(const config_t *config, const char *section)
121 {
122 assert(config != NULL);
123 assert(section != NULL);
124
125 return (section_find(config, section) != NULL);
126 }
127
config_has_key(const config_t * config,const char * section,const char * key)128 bool config_has_key(const config_t *config, const char *section, const char *key)
129 {
130 assert(config != NULL);
131 assert(section != NULL);
132 assert(key != NULL);
133
134 return (entry_find(config, section, key) != NULL);
135 }
136
config_has_key_in_section(config_t * config,const char * key,char * key_value)137 bool config_has_key_in_section(config_t *config, const char *key, char *key_value)
138 {
139 OSI_TRACE_DEBUG("key = %s, value = %s", key, key_value);
140 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
141 const section_t *section = (const section_t *)list_node(node);
142
143 for (const list_node_t *node = list_begin(section->entries); node != list_end(section->entries); node = list_next(node)) {
144 entry_t *entry = list_node(node);
145 OSI_TRACE_DEBUG("entry->key = %s, entry->value = %s", entry->key, entry->value);
146 if (!strcmp(entry->key, key) && !strcmp(entry->value, key_value)) {
147 OSI_TRACE_DEBUG("%s, the irk aready in the flash.", __func__);
148 return true;
149 }
150 }
151 }
152
153 return false;
154 }
155
config_get_int(const config_t * config,const char * section,const char * key,int def_value)156 int config_get_int(const config_t *config, const char *section, const char *key, int def_value)
157 {
158 assert(config != NULL);
159 assert(section != NULL);
160 assert(key != NULL);
161
162 entry_t *entry = entry_find(config, section, key);
163 if (!entry) {
164 return def_value;
165 }
166
167 char *endptr;
168 int ret = strtol(entry->value, &endptr, 0);
169 return (*endptr == '\0') ? ret : def_value;
170 }
171
config_get_bool(const config_t * config,const char * section,const char * key,bool def_value)172 bool config_get_bool(const config_t *config, const char *section, const char *key, bool def_value)
173 {
174 assert(config != NULL);
175 assert(section != NULL);
176 assert(key != NULL);
177
178 entry_t *entry = entry_find(config, section, key);
179 if (!entry) {
180 return def_value;
181 }
182
183 if (!strcmp(entry->value, "true")) {
184 return true;
185 }
186 if (!strcmp(entry->value, "false")) {
187 return false;
188 }
189
190 return def_value;
191 }
192
config_get_string(const config_t * config,const char * section,const char * key,const char * def_value)193 const char *config_get_string(const config_t *config, const char *section, const char *key, const char *def_value)
194 {
195 assert(config != NULL);
196 assert(section != NULL);
197 assert(key != NULL);
198
199 entry_t *entry = entry_find(config, section, key);
200 if (!entry) {
201 return def_value;
202 }
203
204 return entry->value;
205 }
206
config_set_int(config_t * config,const char * section,const char * key,int value)207 void config_set_int(config_t *config, const char *section, const char *key, int value)
208 {
209 assert(config != NULL);
210 assert(section != NULL);
211 assert(key != NULL);
212
213 char value_str[32] = { 0 };
214 sprintf(value_str, "%d", value);
215 config_set_string(config, section, key, value_str, false);
216 }
217
config_set_bool(config_t * config,const char * section,const char * key,bool value)218 void config_set_bool(config_t *config, const char *section, const char *key, bool value)
219 {
220 assert(config != NULL);
221 assert(section != NULL);
222 assert(key != NULL);
223
224 config_set_string(config, section, key, value ? "true" : "false", false);
225 }
226
config_set_string(config_t * config,const char * section,const char * key,const char * value,bool insert_back)227 void config_set_string(config_t *config, const char *section, const char *key, const char *value, bool insert_back)
228 {
229 section_t *sec = section_find(config, section);
230 if (!sec) {
231 sec = section_new(section);
232 if (insert_back) {
233 list_append(config->sections, sec);
234 } else {
235 list_prepend(config->sections, sec);
236 }
237 }
238
239 for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) {
240 entry_t *entry = list_node(node);
241 if (!strcmp(entry->key, key)) {
242 osi_free(entry->value);
243 entry->value = osi_strdup(value);
244 return;
245 }
246 }
247
248 entry_t *entry = entry_new(key, value);
249 list_append(sec->entries, entry);
250 }
251
config_remove_section(config_t * config,const char * section)252 bool config_remove_section(config_t *config, const char *section)
253 {
254 assert(config != NULL);
255 assert(section != NULL);
256
257 section_t *sec = section_find(config, section);
258 if (!sec) {
259 return false;
260 }
261
262 return list_remove(config->sections, sec);
263 }
264
config_remove_key(config_t * config,const char * section,const char * key)265 bool config_remove_key(config_t *config, const char *section, const char *key)
266 {
267 assert(config != NULL);
268 assert(section != NULL);
269 assert(key != NULL);
270 bool ret;
271
272 section_t *sec = section_find(config, section);
273 entry_t *entry = entry_find(config, section, key);
274 if (!sec || !entry) {
275 return false;
276 }
277
278 ret = list_remove(sec->entries, entry);
279 if (list_length(sec->entries) == 0) {
280 OSI_TRACE_DEBUG("%s remove section name:%s",__func__, section);
281 ret &= config_remove_section(config, section);
282 }
283 return ret;
284 }
285
config_section_begin(const config_t * config)286 const config_section_node_t *config_section_begin(const config_t *config)
287 {
288 assert(config != NULL);
289 return (const config_section_node_t *)list_begin(config->sections);
290 }
291
config_section_end(const config_t * config)292 const config_section_node_t *config_section_end(const config_t *config)
293 {
294 assert(config != NULL);
295 return (const config_section_node_t *)list_end(config->sections);
296 }
297
config_section_next(const config_section_node_t * node)298 const config_section_node_t *config_section_next(const config_section_node_t *node)
299 {
300 assert(node != NULL);
301 return (const config_section_node_t *)list_next((const list_node_t *)node);
302 }
303
config_section_name(const config_section_node_t * node)304 const char *config_section_name(const config_section_node_t *node)
305 {
306 assert(node != NULL);
307 const list_node_t *lnode = (const list_node_t *)node;
308 const section_t *section = (const section_t *)list_node(lnode);
309 return section->name;
310 }
311
get_config_size(const config_t * config)312 static int get_config_size(const config_t *config)
313 {
314 assert(config != NULL);
315
316 int w_len = 0, total_size = 0;
317
318 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
319 const section_t *section = (const section_t *)list_node(node);
320 w_len = strlen(section->name) + strlen("[]\n");// format "[section->name]\n"
321 total_size += w_len;
322
323 for (const list_node_t *enode = list_begin(section->entries); enode != list_end(section->entries); enode = list_next(enode)) {
324 const entry_t *entry = (const entry_t *)list_node(enode);
325 w_len = strlen(entry->key) + strlen(entry->value) + strlen(" = \n");// format "entry->key = entry->value\n"
326 total_size += w_len;
327 }
328
329 // Only add a separating newline if there are more sections.
330 if (list_next(node) != list_end(config->sections)) {
331 total_size ++; //'\n'
332 } else {
333 break;
334 }
335 }
336 total_size ++; //'\0'
337 return total_size;
338 }
339
get_config_size_from_flash(nvs_handle_t fp)340 static int get_config_size_from_flash(nvs_handle_t fp)
341 {
342 assert(fp != 0);
343
344 esp_err_t err;
345 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
346 char *keyname = osi_calloc(keyname_bufsz);
347 if (!keyname){
348 OSI_TRACE_ERROR("%s, malloc error\n", __func__);
349 return 0;
350 }
351 size_t length = CONFIG_FILE_DEFAULE_LENGTH;
352 size_t total_length = 0;
353 uint16_t i = 0;
354 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
355 err = nvs_get_blob(fp, keyname, NULL, &length);
356 if (err == ESP_ERR_NVS_NOT_FOUND) {
357 osi_free(keyname);
358 return 0;
359 }
360 if (err != ESP_OK) {
361 OSI_TRACE_ERROR("%s, error %d\n", __func__, err);
362 osi_free(keyname);
363 return 0;
364 }
365 total_length += length;
366 while (length == CONFIG_FILE_MAX_SIZE) {
367 length = CONFIG_FILE_DEFAULE_LENGTH;
368 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, ++i);
369 err = nvs_get_blob(fp, keyname, NULL, &length);
370
371 if (err == ESP_ERR_NVS_NOT_FOUND) {
372 break;
373 }
374 if (err != ESP_OK) {
375 OSI_TRACE_ERROR("%s, error %d\n", __func__, err);
376 osi_free(keyname);
377 return 0;
378 }
379 total_length += length;
380 }
381 osi_free(keyname);
382 return total_length;
383 }
384
config_save(const config_t * config,const char * filename)385 bool config_save(const config_t *config, const char *filename)
386 {
387 assert(config != NULL);
388 assert(filename != NULL);
389 assert(*filename != '\0');
390
391 esp_err_t err;
392 int err_code = 0;
393 nvs_handle_t fp;
394 char *line = osi_calloc(1024);
395 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
396 char *keyname = osi_calloc(keyname_bufsz);
397 int config_size = get_config_size(config);
398 char *buf = osi_calloc(config_size);
399 if (!line || !buf || !keyname) {
400 err_code |= 0x01;
401 goto error;
402 }
403
404 err = nvs_open(filename, NVS_READWRITE, &fp);
405 if (err != ESP_OK) {
406 if (err == ESP_ERR_NVS_NOT_INITIALIZED) {
407 OSI_TRACE_ERROR("%s: NVS not initialized. "
408 "Call nvs_flash_init before initializing bluetooth.", __func__);
409 }
410 err_code |= 0x02;
411 goto error;
412 }
413
414 int w_cnt, w_cnt_total = 0;
415 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
416 const section_t *section = (const section_t *)list_node(node);
417 w_cnt = snprintf(line, 1024, "[%s]\n", section->name);
418 if(w_cnt < 0) {
419 OSI_TRACE_ERROR("snprintf error w_cnt %d.",w_cnt);
420 err_code |= 0x10;
421 goto error;
422 }
423 if(w_cnt_total + w_cnt > config_size) {
424 OSI_TRACE_ERROR("%s, memcpy size (w_cnt + w_cnt_total = %d) is larger than buffer size (config_size = %d).", __func__, (w_cnt + w_cnt_total), config_size);
425 err_code |= 0x20;
426 goto error;
427 }
428 OSI_TRACE_DEBUG("section name: %s, w_cnt + w_cnt_total = %d\n", section->name, w_cnt + w_cnt_total);
429 memcpy(buf + w_cnt_total, line, w_cnt);
430 w_cnt_total += w_cnt;
431
432 for (const list_node_t *enode = list_begin(section->entries); enode != list_end(section->entries); enode = list_next(enode)) {
433 const entry_t *entry = (const entry_t *)list_node(enode);
434 OSI_TRACE_DEBUG("(key, val): (%s, %s)\n", entry->key, entry->value);
435 w_cnt = snprintf(line, 1024, "%s = %s\n", entry->key, entry->value);
436 if(w_cnt < 0) {
437 OSI_TRACE_ERROR("snprintf error w_cnt %d.",w_cnt);
438 err_code |= 0x10;
439 goto error;
440 }
441 if(w_cnt_total + w_cnt > config_size) {
442 OSI_TRACE_ERROR("%s, memcpy size (w_cnt + w_cnt_total = %d) is larger than buffer size.(config_size = %d)", __func__, (w_cnt + w_cnt_total), config_size);
443 err_code |= 0x20;
444 goto error;
445 }
446 OSI_TRACE_DEBUG("%s, w_cnt + w_cnt_total = %d", __func__, w_cnt + w_cnt_total);
447 memcpy(buf + w_cnt_total, line, w_cnt);
448 w_cnt_total += w_cnt;
449 }
450
451 // Only add a separating newline if there are more sections.
452 if (list_next(node) != list_end(config->sections)) {
453 buf[w_cnt_total] = '\n';
454 w_cnt_total += 1;
455 } else {
456 break;
457 }
458 }
459 buf[w_cnt_total] = '\0';
460 if (w_cnt_total < CONFIG_FILE_MAX_SIZE) {
461 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
462 err = nvs_set_blob(fp, keyname, buf, w_cnt_total);
463 if (err != ESP_OK) {
464 nvs_close(fp);
465 err_code |= 0x04;
466 goto error;
467 }
468 }else {
469 int count = (w_cnt_total / CONFIG_FILE_MAX_SIZE);
470 assert(count <= 0xFF);
471 for (uint8_t i = 0; i <= count; i++)
472 {
473 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, i);
474 if (i == count) {
475 err = nvs_set_blob(fp, keyname, buf + i*CONFIG_FILE_MAX_SIZE, w_cnt_total - i*CONFIG_FILE_MAX_SIZE);
476 OSI_TRACE_DEBUG("save keyname = %s, i = %d, %d\n", keyname, i, w_cnt_total - i*CONFIG_FILE_MAX_SIZE);
477 }else {
478 err = nvs_set_blob(fp, keyname, buf + i*CONFIG_FILE_MAX_SIZE, CONFIG_FILE_MAX_SIZE);
479 OSI_TRACE_DEBUG("save keyname = %s, i = %d, %d\n", keyname, i, CONFIG_FILE_MAX_SIZE);
480 }
481 if (err != ESP_OK) {
482 nvs_close(fp);
483 err_code |= 0x04;
484 goto error;
485 }
486 }
487 }
488
489 err = nvs_commit(fp);
490 if (err != ESP_OK) {
491 nvs_close(fp);
492 err_code |= 0x08;
493 goto error;
494 }
495
496 nvs_close(fp);
497 osi_free(line);
498 osi_free(buf);
499 osi_free(keyname);
500 return true;
501
502 error:
503 if (buf) {
504 osi_free(buf);
505 }
506 if (line) {
507 osi_free(line);
508 }
509 if (keyname) {
510 osi_free(keyname);
511 }
512 if (err_code) {
513 OSI_TRACE_ERROR("%s, err_code: 0x%x\n", __func__, err_code);
514 }
515 return false;
516 }
517
trim(char * str)518 static char *trim(char *str)
519 {
520 while (isspace((unsigned char)(*str))) {
521 ++str;
522 }
523
524 if (!*str) {
525 return str;
526 }
527
528 char *end_str = str + strlen(str) - 1;
529 while (end_str > str && isspace((unsigned char)(*end_str))) {
530 --end_str;
531 }
532
533 end_str[1] = '\0';
534 return str;
535 }
536
config_parse(nvs_handle_t fp,config_t * config)537 static void config_parse(nvs_handle_t fp, config_t *config)
538 {
539 assert(fp != 0);
540 assert(config != NULL);
541
542 esp_err_t err;
543 int line_num = 0;
544 int err_code = 0;
545 uint16_t i = 0;
546 size_t length = CONFIG_FILE_DEFAULE_LENGTH;
547 size_t total_length = 0;
548 char *line = osi_calloc(1024);
549 char *section = osi_calloc(1024);
550 const size_t keyname_bufsz = sizeof(CONFIG_KEY) + 5 + 1; // including log10(sizeof(i))
551 char *keyname = osi_calloc(keyname_bufsz);
552 int buf_size = get_config_size_from_flash(fp);
553 char *buf = osi_calloc(buf_size);
554 if(buf_size == 0) { //First use nvs
555 goto error;
556 }
557 if (!line || !section || !buf || !keyname) {
558 err_code |= 0x01;
559 goto error;
560 }
561 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, 0);
562 err = nvs_get_blob(fp, keyname, buf, &length);
563 if (err == ESP_ERR_NVS_NOT_FOUND) {
564 goto error;
565 }
566 if (err != ESP_OK) {
567 err_code |= 0x02;
568 goto error;
569 }
570 total_length += length;
571 while (length == CONFIG_FILE_MAX_SIZE) {
572 length = CONFIG_FILE_DEFAULE_LENGTH;
573 snprintf(keyname, keyname_bufsz, "%s%d", CONFIG_KEY, ++i);
574 err = nvs_get_blob(fp, keyname, buf + CONFIG_FILE_MAX_SIZE * i, &length);
575
576 if (err == ESP_ERR_NVS_NOT_FOUND) {
577 break;
578 }
579 if (err != ESP_OK) {
580 err_code |= 0x02;
581 goto error;
582 }
583 total_length += length;
584 }
585 char *p_line_end;
586 char *p_line_bgn = buf;
587 strcpy(section, CONFIG_DEFAULT_SECTION);
588
589 while ( (p_line_bgn < buf + total_length - 1) && (p_line_end = strchr(p_line_bgn, '\n'))) {
590
591 // get one line
592 int line_len = p_line_end - p_line_bgn;
593 if (line_len > 1023) {
594 OSI_TRACE_WARNING("%s exceed max line length on line %d.\n", __func__, line_num);
595 break;
596 }
597 memcpy(line, p_line_bgn, line_len);
598 line[line_len] = '\0';
599 p_line_bgn = p_line_end + 1;
600 char *line_ptr = trim(line);
601 ++line_num;
602
603 // Skip blank and comment lines.
604 if (*line_ptr == '\0' || *line_ptr == '#') {
605 continue;
606 }
607
608 if (*line_ptr == '[') {
609 size_t len = strlen(line_ptr);
610 if (line_ptr[len - 1] != ']') {
611 OSI_TRACE_WARNING("%s unterminated section name on line %d.\n", __func__, line_num);
612 continue;
613 }
614 strncpy(section, line_ptr + 1, len - 2);
615 section[len - 2] = '\0';
616 } else {
617 char *split = strchr(line_ptr, '=');
618 if (!split) {
619 OSI_TRACE_DEBUG("%s no key/value separator found on line %d.\n", __func__, line_num);
620 continue;
621 }
622 *split = '\0';
623 config_set_string(config, section, trim(line_ptr), trim(split + 1), true);
624 }
625 }
626
627 error:
628 if (buf) {
629 osi_free(buf);
630 }
631 if (line) {
632 osi_free(line);
633 }
634 if (section) {
635 osi_free(section);
636 }
637 if (keyname) {
638 osi_free(keyname);
639 }
640 if (err_code) {
641 OSI_TRACE_ERROR("%s returned with err code: %d\n", __func__, err_code);
642 }
643 }
644
section_new(const char * name)645 static section_t *section_new(const char *name)
646 {
647 section_t *section = osi_calloc(sizeof(section_t));
648 if (!section) {
649 return NULL;
650 }
651
652 section->name = osi_strdup(name);
653 section->entries = list_new(entry_free);
654 return section;
655 }
656
section_free(void * ptr)657 static void section_free(void *ptr)
658 {
659 if (!ptr) {
660 return;
661 }
662
663 section_t *section = ptr;
664 osi_free(section->name);
665 list_free(section->entries);
666 osi_free(section);
667 }
668
section_find(const config_t * config,const char * section)669 static section_t *section_find(const config_t *config, const char *section)
670 {
671 for (const list_node_t *node = list_begin(config->sections); node != list_end(config->sections); node = list_next(node)) {
672 section_t *sec = list_node(node);
673 if (!strcmp(sec->name, section)) {
674 return sec;
675 }
676 }
677
678 return NULL;
679 }
680
entry_new(const char * key,const char * value)681 static entry_t *entry_new(const char *key, const char *value)
682 {
683 entry_t *entry = osi_calloc(sizeof(entry_t));
684 if (!entry) {
685 return NULL;
686 }
687
688 entry->key = osi_strdup(key);
689 entry->value = osi_strdup(value);
690 return entry;
691 }
692
entry_free(void * ptr)693 static void entry_free(void *ptr)
694 {
695 if (!ptr) {
696 return;
697 }
698
699 entry_t *entry = ptr;
700 osi_free(entry->key);
701 osi_free(entry->value);
702 osi_free(entry);
703 }
704
entry_find(const config_t * config,const char * section,const char * key)705 static entry_t *entry_find(const config_t *config, const char *section, const char *key)
706 {
707 section_t *sec = section_find(config, section);
708 if (!sec) {
709 return NULL;
710 }
711
712 for (const list_node_t *node = list_begin(sec->entries); node != list_end(sec->entries); node = list_next(node)) {
713 entry_t *entry = list_node(node);
714 if (!strcmp(entry->key, key)) {
715 return entry;
716 }
717 }
718
719 return NULL;
720 }
721