├── .github └── workflows │ ├── build.yml │ └── upload_component.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── example ├── CMakeLists.txt ├── main │ ├── CMakeLists.txt │ ├── Kconfig.projbuild │ ├── idf_component.yml │ ├── nmea_example.h │ ├── nmea_example_i2c.c │ ├── nmea_example_main.c │ └── nmea_example_uart.c ├── sdkconfig.ci └── test_example.py ├── idf_component.yml └── pytest.ini /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'build' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | schedule: 10 | - cron: '0 1 * * 6' 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | idf_ver: ["release-v4.2", "release-v4.3", "release-v4.4", "release-v5.0", "latest"] 17 | idf_target: ["esp32"] 18 | include: 19 | - idf_ver: "release-v4.2" 20 | idf_target: esp32s2 21 | - idf_ver: "release-v4.3" 22 | idf_target: esp32c3 23 | - idf_ver: "release-v4.4" 24 | idf_target: esp32s3 25 | - idf_ver: "release-v5.0" 26 | idf_target: esp32c2 27 | runs-on: ubuntu-22.04 28 | container: espressif/idf:${{ matrix.idf_ver }} 29 | steps: 30 | - uses: actions/checkout@v1 31 | with: 32 | submodules: recursive 33 | # idf_component.yml "override_path" feature requires the component directory name 34 | # to match the component name in the registry, so use 'libnmea' instead of 'libnmea-esp32' 35 | # when checking it out 36 | path: libnmea 37 | - name: Build for ${{ matrix.idf_target }} 38 | env: 39 | IDF_TARGET: ${{ matrix.idf_target }} 40 | shell: bash 41 | working-directory: example 42 | run: | 43 | . ${IDF_PATH}/export.sh 44 | idf.py build 45 | 46 | run-qemu: 47 | strategy: 48 | matrix: 49 | idf_ver: ["latest"] 50 | idf_target: ["esp32"] 51 | runs-on: ubuntu-22.04 52 | container: espressif/idf:${{ matrix.idf_ver }} 53 | steps: 54 | - uses: actions/checkout@v1 55 | with: 56 | submodules: recursive 57 | # see the comment about checkout path in the job above 58 | path: libnmea 59 | - name: Install pytest 60 | shell: bash 61 | run: | 62 | . ${IDF_PATH}/export.sh 63 | pip install pytest-embedded pytest-embedded-qemu pytest-embedded-idf pytest-embedded-serial pytest-embedded-serial-esp 64 | - name: Build the example 65 | shell: bash 66 | working-directory: example 67 | run: | 68 | . ${IDF_PATH}/export.sh 69 | idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.ci" build 70 | - name: Run the tests 71 | shell: bash 72 | working-directory: example 73 | run: | 74 | . ${IDF_PATH}/export.sh 75 | pytest 76 | -------------------------------------------------------------------------------- /.github/workflows/upload_component.yml: -------------------------------------------------------------------------------- 1 | name: Push the component to https://components.espressif.com 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | jobs: 8 | upload_components: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | submodules: "recursive" 14 | 15 | - name: Upload component to the component registry 16 | uses: espressif/upload-components-ci-action@v1 17 | with: 18 | version: ${{ github.ref_name }} 19 | name: "libnmea" 20 | namespace: "igrr" 21 | api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .cproject 3 | .DS_Store 4 | .idea 5 | .project 6 | .pytest_cache 7 | .settings 8 | .vscode 9 | dependencies.lock 10 | dist/ 11 | example/build 12 | example/sdkconfig 13 | example/sdkconfig.old 14 | flash_image.bin 15 | sdkconfig.old 16 | test.log 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libnmea"] 2 | path = libnmea 3 | url = https://github.com/jacketizer/libnmea 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-executables-have-shebangs 8 | - id: mixed-line-ending 9 | args: ['-f=lf'] 10 | - repo: https://github.com/igrr/astyle_py.git 11 | rev: v0.9.0 12 | hooks: 13 | - id: astyle_py 14 | args: ['--style=otbs', '--attach-namespaces', '--attach-classes', '--indent=spaces=4', '--convert-tabs', '--align-pointer=name', '--align-reference=name', '--keep-one-line-statements', '--pad-header', '--pad-oper'] 15 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | set(common "libnmea/src/nmea/nmea.c" 4 | "libnmea/src/nmea/parser_static.c" 5 | "libnmea/src/parsers/parse.c" 6 | ) 7 | 8 | idf_component_register(INCLUDE_DIRS "libnmea/src/nmea" "libnmea/src/parsers" 9 | SRCS ${common}) 10 | 11 | set(parsers gpgga gpgll gprmc gpgsa gpgsv gptxt gpvtg) 12 | 13 | foreach(parser ${parsers}) 14 | # add source file 15 | set(src_file "libnmea/src/parsers/${parser}.c") 16 | target_sources(${COMPONENT_TARGET} PRIVATE ${src_file}) 17 | 18 | # add some preprocessor definitions to rename the interface functions 19 | set(prefix nmea_${parser}_) 20 | set(defs "allocate_data=${prefix}allocate_data" 21 | "free_data=${prefix}free_data" 22 | "init=${prefix}init" 23 | "parse=${prefix}parse" 24 | "set_default=${prefix}set_default" 25 | ) 26 | set_source_files_properties(${src_file} PROPERTIES COMPILE_DEFINITIONS "${defs}") 27 | 28 | # Enable the parser 29 | string(TOUPPER ${parser} parser_uppercase) 30 | target_compile_definitions(${COMPONENT_TARGET} PRIVATE "ENABLE_${parser_uppercase}=1") 31 | endforeach() 32 | 33 | list(LENGTH parsers parsers_count) 34 | target_compile_definitions(${COMPONENT_TARGET} PRIVATE PARSER_COUNT=${parsers_count}) 35 | target_compile_options(${COMPONENT_TARGET} PRIVATE "-Wno-strict-prototypes") 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jack Engqvist Johansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Component Registry](https://components.espressif.com/components/igrr/libnmea/badge.svg)](https://components.espressif.com/components/igrr/libnmea) 2 | 3 | # NMEA parser component for ESP-IDF 4 | 5 | This is a wrapper around [libnmea](https://github.com/jacketizer/libnmea), in the form of an [ESP-IDF](https://github.com/espressif/esp-idf) component. It works with any chip supported in ESP-IDF: ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C2, ESP32-C6, ESP32-H2. 6 | 7 | ## Usage 8 | 9 | This component uses CMake build system. It works with ESP-IDF v4.2 or later. 10 | 11 | See [libnmea documentation](https://github.com/jacketizer/libnmea#how-to-use-it) for more details about libnmea API. 12 | 13 | There are two ways to use this component: 14 | 15 | 1. Use [idf-component-manager](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html). Simply run `idf.py add-dependency igrr/libnmea==required_version` in your project directory, changing `required_version` to the version you want to install. 16 | 17 | 2. Clone the component into the `components` directory of your project, or add it as a submodule. 18 | 19 | ## Example 20 | 21 | Example project is provided inside `example` directory. It works the same way as `parse_stdin.c` example from libnmea, except that it reads NMEA sentences from UART or I2C. 22 | 23 | Configure the example as explained in the sections below, then build and flash it. Decoded NMEA messages will be displayed in the console. 24 | 25 | ### Using with a UART connected GPS 26 | 27 | Connect the TXD pin of GPS receiver to GPIO21 of an ESP32 board. You can change the number to any other unused GPIO. The pin number can be changed in menuconfig (under "libnmea example configuration" menu) or directly in the code. 28 | 29 | ### Using with an I2C connected GPS 30 | 31 | The example also works with an I2C connected PA1010D GPS module (e.g. [this one](https://www.adafruit.com/product/4415)). To use I2C interface instead of UART, select it in menuconfig under "libnmea example configuration" menu. Set the SDA and SCL pin numbers, as well as the I2C address of the module. 32 | 33 | ## License 34 | 35 | [libnmea](https://github.com/jacketizer/libnmea), this component, and the 36 | example project are licensed under MIT License. 37 | -------------------------------------------------------------------------------- /example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 4 | project(nmea_example) 5 | -------------------------------------------------------------------------------- /example/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS nmea_example_main.c) 2 | 3 | if(CONFIG_EXAMPLE_NMEA_INTERFACE_UART) 4 | target_sources(${COMPONENT_LIB} PRIVATE nmea_example_uart.c) 5 | elseif(CONFIG_EXAMPLE_NMEA_INTERFACE_I2C) 6 | target_sources(${COMPONENT_LIB} PRIVATE nmea_example_i2c.c) 7 | endif() 8 | -------------------------------------------------------------------------------- /example/main/Kconfig.projbuild: -------------------------------------------------------------------------------- 1 | menu "libnmea Example Settings" 2 | 3 | choice EXAMPLE_NMEA_INTERFACE 4 | prompt "Interface to connect the GPS over" 5 | default EXAMPLE_NMEA_INTERFACE_UART 6 | 7 | config EXAMPLE_NMEA_INTERFACE_UART 8 | bool "UART" 9 | 10 | config EXAMPLE_NMEA_INTERFACE_I2C 11 | bool "I2C" 12 | 13 | endchoice 14 | 15 | if EXAMPLE_NMEA_INTERFACE_UART 16 | config EXAMPLE_UART_NUM 17 | int "UART port number" 18 | default 1 19 | 20 | config EXAMPLE_UART_RX 21 | int "UART RX GPIO number" 22 | default 21 23 | endif 24 | 25 | if EXAMPLE_NMEA_INTERFACE_I2C 26 | config EXAMPLE_I2C_NUM 27 | int "I2C port number" 28 | default 0 29 | config EXAMPLE_I2C_SDA_PIN 30 | int "I2C SDA GPIO number" 31 | default 24 32 | config EXAMPLE_I2C_SCL_PIN 33 | int "I2C SCL GPIO number" 34 | default 25 35 | config EXAMPLE_I2C_SLAVE_ADDR 36 | hex "I2C address of the GPS" 37 | default 0x10 38 | endif 39 | 40 | 41 | endmenu 42 | -------------------------------------------------------------------------------- /example/main/idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | igrr/libnmea: 3 | version: "*" 4 | override_path: "../../" 5 | -------------------------------------------------------------------------------- /example/main/nmea_example.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /** 6 | * @brief Initialize the interface connected to the GPS (I2C or UART) 7 | */ 8 | void nmea_example_init_interface(void); 9 | 10 | /** 11 | * @brief Get one NMEA message line from the GPS. 12 | * 13 | * Note, the returned buffer pointer is only valid until the next call to this function. 14 | * If no data was received, out_line_len is zero. 15 | * 16 | * @param[out] out_line_buf output, pointer to the buffer containing the NMEA message 17 | * @param[out[ out_line_len length of the message, in bytes 18 | * @param[in] timeout_ms timeout for reading the message, in milliseconds 19 | */ 20 | void nmea_example_read_line(char **out_line_buf, size_t *out_line_len, int timeout_ms); 21 | -------------------------------------------------------------------------------- /example/main/nmea_example_i2c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "esp_log.h" 3 | #include "freertos/FreeRTOS.h" 4 | #include "freertos/task.h" 5 | #include "driver/i2c.h" 6 | #include "sdkconfig.h" 7 | #include "nmea_example.h" 8 | #include "nmea.h" 9 | 10 | #define I2C_NUM CONFIG_EXAMPLE_I2C_NUM 11 | #define I2C_SDA_PIN CONFIG_EXAMPLE_I2C_SDA_PIN 12 | #define I2C_SCL_PIN CONFIG_EXAMPLE_I2C_SCL_PIN 13 | #define I2C_FREQ_HZ 100000 14 | #define I2C_RX_BUF_SIZE (1024) 15 | #define I2C_SLAVE_ADDR CONFIG_EXAMPLE_I2C_SLAVE_ADDR 16 | 17 | 18 | static char s_buf[I2C_RX_BUF_SIZE + 1]; 19 | static size_t s_total_bytes; 20 | 21 | static uint8_t s_i2c_link_buf[I2C_LINK_RECOMMENDED_SIZE(2)]; 22 | 23 | static const char *TAG = "example_i2c"; 24 | 25 | void nmea_example_init_interface(void) 26 | { 27 | const i2c_config_t conf = { 28 | .mode = I2C_MODE_MASTER, 29 | .sda_io_num = I2C_SDA_PIN, 30 | .sda_pullup_en = GPIO_PULLUP_ENABLE, 31 | .scl_io_num = I2C_SCL_PIN, 32 | .scl_pullup_en = GPIO_PULLUP_ENABLE, 33 | .master.clk_speed = I2C_FREQ_HZ, 34 | }; 35 | ESP_ERROR_CHECK(i2c_param_config(I2C_NUM, &conf)); 36 | ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM, conf.mode, 0, 0, 0)); 37 | } 38 | 39 | static esp_err_t i2c_read_byte(char *result) 40 | { 41 | i2c_cmd_handle_t cmd = i2c_cmd_link_create_static(s_i2c_link_buf, sizeof(s_i2c_link_buf)); 42 | i2c_master_start(cmd); 43 | i2c_master_write_byte(cmd, (I2C_SLAVE_ADDR << 1) | I2C_MASTER_READ, true); 44 | i2c_master_read_byte(cmd, (uint8_t *) result, 1); 45 | i2c_master_stop(cmd); 46 | esp_err_t err = i2c_master_cmd_begin(I2C_NUM, cmd, 1000 / portTICK_PERIOD_MS); 47 | i2c_cmd_link_delete_static(cmd); 48 | return err; 49 | } 50 | 51 | void nmea_example_read_line(char **out_line_buf, size_t *out_line_len, int timeout_ms) 52 | { 53 | *out_line_buf = NULL; 54 | *out_line_len = 0; 55 | 56 | bool line_start_found = false; 57 | 58 | while (true) { 59 | char b; 60 | esp_err_t err = i2c_read_byte(&b); 61 | if (err != ESP_OK) { 62 | ESP_LOGE(TAG, "Failed to read byte over I2C: %s (0x%x)", esp_err_to_name(err), err); 63 | return; 64 | } 65 | 66 | if (b == 0) { 67 | /* No byte available yet, wait a bit and retry */ 68 | vTaskDelay(pdMS_TO_TICKS(10)); 69 | continue; 70 | } 71 | 72 | if (!line_start_found) { 73 | if (b != '$') { 74 | continue; 75 | } else { 76 | line_start_found = true; 77 | } 78 | } 79 | 80 | s_buf[s_total_bytes] = b; 81 | ++s_total_bytes; 82 | 83 | if (b == '\n') { 84 | *out_line_buf = s_buf; 85 | *out_line_len = s_total_bytes; 86 | s_total_bytes = 0; 87 | return; 88 | } 89 | 90 | if (s_total_bytes == sizeof(s_buf)) { 91 | ESP_LOGE(TAG, "Line buffer overflow"); 92 | s_total_bytes = 0; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/main/nmea_example_main.c: -------------------------------------------------------------------------------- 1 | /* NMEA parsing example for ESP32. 2 | * Based on "parse_stdin.c" example from libnmea. 3 | * Copyright (c) 2015 Jack Engqvist Johansson. 4 | * Additions Copyright (c) 2017 Ivan Grokhotkov. 5 | * See "LICENSE" file in libnmea directory for license. 6 | */ 7 | 8 | #include 9 | #include "nmea_example.h" 10 | #include "nmea.h" 11 | #include "gpgll.h" 12 | #include "gpgga.h" 13 | #include "gprmc.h" 14 | #include "gpgsa.h" 15 | #include "gpvtg.h" 16 | #include "gptxt.h" 17 | #include "gpgsv.h" 18 | 19 | static void read_and_parse_nmea(); 20 | 21 | void app_main() 22 | { 23 | nmea_example_init_interface(); 24 | read_and_parse_nmea(); 25 | } 26 | 27 | 28 | static void read_and_parse_nmea() 29 | { 30 | printf("Example ready\n"); 31 | while (1) { 32 | char fmt_buf[32]; 33 | nmea_s *data; 34 | 35 | char *start; 36 | size_t length; 37 | nmea_example_read_line(&start, &length, 100 /* ms */); 38 | if (length == 0) { 39 | continue; 40 | } 41 | 42 | /* handle data */ 43 | data = nmea_parse(start, length, 0); 44 | if (data == NULL) { 45 | printf("Failed to parse the sentence!\n"); 46 | printf(" Type: %.5s (%d)\n", start + 1, nmea_get_type(start)); 47 | } else { 48 | if (data->errors != 0) { 49 | printf("WARN: The sentence struct contains parse errors!\n"); 50 | } 51 | 52 | if (NMEA_GPGGA == data->type) { 53 | printf("GPGGA sentence\n"); 54 | nmea_gpgga_s *gpgga = (nmea_gpgga_s *) data; 55 | printf("Number of satellites: %d\n", gpgga->n_satellites); 56 | printf("Altitude: %f %c\n", gpgga->altitude, 57 | gpgga->altitude_unit); 58 | } 59 | 60 | if (NMEA_GPGLL == data->type) { 61 | printf("GPGLL sentence\n"); 62 | nmea_gpgll_s *pos = (nmea_gpgll_s *) data; 63 | printf("Longitude:\n"); 64 | printf(" Degrees: %d\n", pos->longitude.degrees); 65 | printf(" Minutes: %f\n", pos->longitude.minutes); 66 | printf(" Cardinal: %c\n", (char) pos->longitude.cardinal); 67 | printf("Latitude:\n"); 68 | printf(" Degrees: %d\n", pos->latitude.degrees); 69 | printf(" Minutes: %f\n", pos->latitude.minutes); 70 | printf(" Cardinal: %c\n", (char) pos->latitude.cardinal); 71 | strftime(fmt_buf, sizeof(fmt_buf), "%H:%M:%S", &pos->time); 72 | printf("Time: %s\n", fmt_buf); 73 | } 74 | 75 | if (NMEA_GPRMC == data->type) { 76 | printf("GPRMC sentence\n"); 77 | nmea_gprmc_s *pos = (nmea_gprmc_s *) data; 78 | printf("Longitude:\n"); 79 | printf(" Degrees: %d\n", pos->longitude.degrees); 80 | printf(" Minutes: %f\n", pos->longitude.minutes); 81 | printf(" Cardinal: %c\n", (char) pos->longitude.cardinal); 82 | printf("Latitude:\n"); 83 | printf(" Degrees: %d\n", pos->latitude.degrees); 84 | printf(" Minutes: %f\n", pos->latitude.minutes); 85 | printf(" Cardinal: %c\n", (char) pos->latitude.cardinal); 86 | strftime(fmt_buf, sizeof(fmt_buf), "%d %b %T %Y", &pos->date_time); 87 | printf("Date & Time: %s\n", fmt_buf); 88 | printf("Speed, in Knots: %f\n", pos->gndspd_knots); 89 | printf("Track, in degrees: %f\n", pos->track_deg); 90 | printf("Magnetic Variation:\n"); 91 | printf(" Degrees: %f\n", pos->magvar_deg); 92 | printf(" Cardinal: %c\n", (char) pos->magvar_cardinal); 93 | double adjusted_course = pos->track_deg; 94 | if (NMEA_CARDINAL_DIR_EAST == pos->magvar_cardinal) { 95 | adjusted_course -= pos->magvar_deg; 96 | } else if (NMEA_CARDINAL_DIR_WEST == pos->magvar_cardinal) { 97 | adjusted_course += pos->magvar_deg; 98 | } else { 99 | printf("Invalid Magnetic Variation Direction!\n"); 100 | } 101 | 102 | printf("Adjusted Track (heading): %f\n", adjusted_course); 103 | } 104 | 105 | if (NMEA_GPGSA == data->type) { 106 | nmea_gpgsa_s *gpgsa = (nmea_gpgsa_s *) data; 107 | 108 | printf("GPGSA Sentence:\n"); 109 | printf(" Mode: %c\n", gpgsa->mode); 110 | printf(" Fix: %d\n", gpgsa->fixtype); 111 | printf(" PDOP: %.2lf\n", gpgsa->pdop); 112 | printf(" HDOP: %.2lf\n", gpgsa->hdop); 113 | printf(" VDOP: %.2lf\n", gpgsa->vdop); 114 | } 115 | 116 | if (NMEA_GPGSV == data->type) { 117 | nmea_gpgsv_s *gpgsv = (nmea_gpgsv_s *) data; 118 | 119 | printf("GPGSV Sentence:\n"); 120 | printf(" Num: %d\n", gpgsv->sentences); 121 | printf(" ID: %d\n", gpgsv->sentence_number); 122 | printf(" SV: %d\n", gpgsv->satellites); 123 | printf(" #1: %d %d %d %d\n", gpgsv->sat[0].prn, gpgsv->sat[0].elevation, gpgsv->sat[0].azimuth, gpgsv->sat[0].snr); 124 | printf(" #2: %d %d %d %d\n", gpgsv->sat[1].prn, gpgsv->sat[1].elevation, gpgsv->sat[1].azimuth, gpgsv->sat[1].snr); 125 | printf(" #3: %d %d %d %d\n", gpgsv->sat[2].prn, gpgsv->sat[2].elevation, gpgsv->sat[2].azimuth, gpgsv->sat[2].snr); 126 | printf(" #4: %d %d %d %d\n", gpgsv->sat[3].prn, gpgsv->sat[3].elevation, gpgsv->sat[3].azimuth, gpgsv->sat[3].snr); 127 | } 128 | 129 | if (NMEA_GPTXT == data->type) { 130 | nmea_gptxt_s *gptxt = (nmea_gptxt_s *) data; 131 | 132 | printf("GPTXT Sentence:\n"); 133 | printf(" ID: %d %d %d\n", gptxt->id_00, gptxt->id_01, gptxt->id_02); 134 | printf(" %s\n", gptxt->text); 135 | } 136 | 137 | if (NMEA_GPVTG == data->type) { 138 | nmea_gpvtg_s *gpvtg = (nmea_gpvtg_s *) data; 139 | 140 | printf("GPVTG Sentence:\n"); 141 | printf(" Track [deg]: %.2lf\n", gpvtg->track_deg); 142 | printf(" Speed [kmph]: %.2lf\n", gpvtg->gndspd_kmph); 143 | printf(" Speed [knots]: %.2lf\n", gpvtg->gndspd_knots); 144 | } 145 | 146 | nmea_free(data); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /example/main/nmea_example_uart.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "freertos/FreeRTOS.h" 3 | #include "freertos/task.h" 4 | #include "driver/uart.h" 5 | #include "sdkconfig.h" 6 | #include "nmea_example.h" 7 | #include "nmea.h" 8 | #include "esp_idf_version.h" 9 | 10 | #define UART_NUM CONFIG_EXAMPLE_UART_NUM 11 | #define UART_RX_PIN CONFIG_EXAMPLE_UART_RX 12 | #define UART_RX_BUF_SIZE (1024) 13 | 14 | 15 | static char s_buf[UART_RX_BUF_SIZE + 1]; 16 | static size_t s_total_bytes; 17 | static char *s_last_buf_end; 18 | 19 | void nmea_example_init_interface(void) 20 | { 21 | uart_config_t uart_config = { 22 | .baud_rate = 9600, 23 | .data_bits = UART_DATA_8_BITS, 24 | .parity = UART_PARITY_DISABLE, 25 | .stop_bits = UART_STOP_BITS_1, 26 | .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, 27 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) 28 | .source_clk = UART_SCLK_DEFAULT, 29 | #endif 30 | }; 31 | ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config)); 32 | ESP_ERROR_CHECK(uart_set_pin(UART_NUM, 33 | UART_PIN_NO_CHANGE, UART_RX_PIN, 34 | UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); 35 | ESP_ERROR_CHECK(uart_driver_install(UART_NUM, UART_RX_BUF_SIZE * 2, 0, 0, NULL, 0)); 36 | } 37 | 38 | void nmea_example_read_line(char **out_line_buf, size_t *out_line_len, int timeout_ms) 39 | { 40 | *out_line_buf = NULL; 41 | *out_line_len = 0; 42 | 43 | if (s_last_buf_end != NULL) { 44 | /* Data left at the end of the buffer after the last call; 45 | * copy it to the beginning. 46 | */ 47 | size_t len_remaining = s_total_bytes - (s_last_buf_end - s_buf); 48 | memmove(s_buf, s_last_buf_end, len_remaining); 49 | s_last_buf_end = NULL; 50 | s_total_bytes = len_remaining; 51 | } 52 | 53 | /* Read data from the UART */ 54 | int read_bytes = uart_read_bytes(UART_NUM, 55 | (uint8_t *) s_buf + s_total_bytes, 56 | UART_RX_BUF_SIZE - s_total_bytes, pdMS_TO_TICKS(timeout_ms)); 57 | if (read_bytes <= 0) { 58 | return; 59 | } 60 | s_total_bytes += read_bytes; 61 | 62 | /* find start (a dollar sign) */ 63 | char *start = memchr(s_buf, '$', s_total_bytes); 64 | if (start == NULL) { 65 | s_total_bytes = 0; 66 | return; 67 | } 68 | 69 | /* find end of line */ 70 | char *end = memchr(start, '\r', s_total_bytes - (start - s_buf)); 71 | if (end == NULL || *(++end) != '\n') { 72 | return; 73 | } 74 | end++; 75 | 76 | end[-2] = NMEA_END_CHAR_1; 77 | end[-1] = NMEA_END_CHAR_2; 78 | 79 | *out_line_buf = start; 80 | *out_line_len = end - start; 81 | if (end < s_buf + s_total_bytes) { 82 | /* some data left at the end of the buffer, record its position until the next call */ 83 | s_last_buf_end = end; 84 | } else { 85 | s_total_bytes = 0; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /example/sdkconfig.ci: -------------------------------------------------------------------------------- 1 | CONFIG_EXAMPLE_UART_NUM=0 2 | CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF=y 3 | -------------------------------------------------------------------------------- /example/test_example.py: -------------------------------------------------------------------------------- 1 | def test_example(dut): 2 | dut.expect_exact("Example ready") 3 | dut.write(b"$GPRMC,170058.89,V,3554.928,N,08002.496,W,9.4,1.57,050521,,E*41\r\n") 4 | dut.expect_exact( 5 | """ 6 | GPRMC sentence 7 | Longitude: 8 | Degrees: 80 9 | Minutes: 2.496000 10 | Cardinal: W 11 | Latitude: 12 | Degrees: 35 13 | Minutes: 54.928000 14 | Cardinal: N 15 | Date & Time: 05 May 17:00:58 2021 16 | Speed, in Knots: 9.400000 17 | Track, in degrees: 1.570000 18 | Magnetic Variation: 19 | Degrees: 0.000000 20 | Cardinal: E 21 | Adjusted Track (heading): 1.570000 22 | """.strip(), 23 | timeout=5, 24 | ) 25 | -------------------------------------------------------------------------------- /idf_component.yml: -------------------------------------------------------------------------------- 1 | description: NMEA parser 2 | url: https://github.com/igrr/libnmea-esp32 3 | repository: https://github.com/igrr/libnmea-esp32.git 4 | documentation: https://github.com/jacketizer/libnmea#how-to-use-it 5 | issues: https://github.com/igrr/libnmea-esp32/issues 6 | discussion: https://github.com/igrr/libnmea-esp32/discussions 7 | dependencies: 8 | idf: 9 | version: ">=4.2" 10 | examples: 11 | - path: ./example 12 | files: 13 | exclude: 14 | - "example/sdkconfig.ci" 15 | - "example/test_*.py" 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --embedded-services idf,qemu 4 | -s 5 | --tb short 6 | 7 | # log related 8 | log_cli = True 9 | log_cli_level = INFO 10 | log_cli_format = %(asctime)s %(levelname)s %(message)s 11 | log_cli_date_format = %Y-%m-%d %H:%M:%S 12 | 13 | log_file = test.log 14 | log_file_level = INFO 15 | log_file_format = %(asctime)s %(levelname)s %(message)s 16 | log_file_date_format = %Y-%m-%d %H:%M:%S 17 | --------------------------------------------------------------------------------