{"id":51374198,"url":"https://github.com/octaprog7/light-nmea-micropython","last_synced_at":"2026-07-03T10:13:55.136Z","repository":{"id":368934451,"uuid":"1286467990","full_name":"octaprog7/light-nmea-micropython","owner":"octaprog7","description":"Fast, NMEA 0183 parser for Python and MicroPython.","archived":false,"fork":false,"pushed_at":"2026-07-02T21:20:28.000Z","size":84,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-02T22:26:08.668Z","etag":null,"topics":["glonass","gnss","gps","micropython","nmea","nmea0183","python3","uart"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/octaprog7.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-01T20:14:49.000Z","updated_at":"2026-07-02T21:20:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/octaprog7/light-nmea-micropython","commit_stats":null,"previous_names":["octaprog7/light-nmea-micropython"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/octaprog7/light-nmea-micropython","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/octaprog7%2Flight-nmea-micropython","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/octaprog7%2Flight-nmea-micropython/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/octaprog7%2Flight-nmea-micropython/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/octaprog7%2Flight-nmea-micropython/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/octaprog7","download_url":"https://codeload.github.com/octaprog7/light-nmea-micropython/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/octaprog7%2Flight-nmea-micropython/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35081598,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-03T02:00:05.635Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["glonass","gnss","gps","micropython","nmea","nmea0183","python3","uart"],"created_at":"2026-07-03T10:13:54.392Z","updated_at":"2026-07-03T10:13:55.124Z","avatar_url":"https://github.com/octaprog7.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LightNMEA\n\n📖 [По-русски](README_RU.md)\n\n\nExtremely fast, zero-dependency, single-pass NMEA 0183 parser with minimal memory allocation, written specifically for MicroPython and Python (CPython).\nDesigned for 32-bit microcontrollers (ESP32, STM32, RP2040) working with high-rate GNSS receivers (10-50 Hz multi-constellation GPS/GLONASS/BeiDou/Galileo modules).\nWhy light_nmea?\nAt the time of creation (2026), there was only one NMEA parser available for MicroPython -- micropyGPS (2018). It works, but is slow (25 packets/sec) and does not support modern GNSS constellations.\nlight_nmea was built as an alternative:\n\n* 15x(!) faster than micropyGPS\n* Support for seven constellations (GPS, GLONASS, Galileo, BeiDou, QZSS, NavIC, Multi-GNSS)\n* Zero memory allocation in the parsing loop\n* MCU RTC integration (MicroPython)\n* Non-blocking UART stream reading\n\nOther popular parsers (pynmea2, pynmeagps) do not work on MicroPython due to dependencies on CPython-specific modules (dataclasses, typing).\n\n## Benchmark\n\n### Benchmark Methodology\n\nTo ensure a fair and comprehensive comparison, the benchmark script implements the following conditions:\n* **Real-World Payload**: Tests are executed using a representative stream of 300,000 to 1,000,000 NMEA 0183 packets (including standard `RMC` and `GGA` sentences).\n* **Warm-up Phase**: Every parser undergoes a warm-up phase (2 complete runs) before the actual measurement to eliminate interpreter initialization overhead.\n* **Property Access Validation**: The benchmark does not just call the parser function; it executes a strict property access simulation (`_ = gps.valid` and `_ = gps.latitude`) to force the code to perform complete data decoding and validation.\n* **Best of 3**: The final results are based on the minimum elapsed time across 3 independent runs, preventing operating system background task spikes from affecting the data.\n* **Garbage Collection**: Explicit `gc.collect()` is invoked before each run to ensure precise memory state baseline.\n\n\n### Benchmark Results (300,000 Packets)\n\n\n| Platform / Environment            |  `light_nmea`   | Competitor                 |       Advantage       |\n|:----------------------------------|:---------------:|:---------------------------|:---------------------:|\n| **CPython** (Ryzen 7 2700)        | **122,861 pps** | `pynmea2`: 81,976 pps      |    **1.5× faster**    |\n| **CPython** (Ryzen 7 2700)        | **124,991 pps** | `pynmeagps`: 48,845 pps    |    **2.6× faster**    |\n| **CPython** (Ryzen 7 2700)        | **125,000 pps** | `adafruit_gps`: 76,277 pps |    **1.6× faster**    |\n| **CPython** (Ryzen 7 2700)        | **127,143 pps** | `micropyGPS`: 35,322 pps   |    **3.6× faster**    |\n| **MicroPython** (RP2040 @ 133MHz) |   **375 pps**   | `micropyGPS`: 25 pps       | **12.0-15.0× faster** |\n\n\n\u003e **Note:** `pynmea2`, `pynmeagps`, and `adafruit_gps` do not work on MicroPython due to\n\u003e dependencies on CPython modules (`dataclasses`, extended `typing`, `circuitpython_typing`).\n\u003e `micropyGPS` is the only alternative NMEA parser for MicroPython, but it has not been updated\n\u003e since 2018 and is significantly slower. On CPython, the gap is smaller (×3.6), but on\n\u003e RP2040 microcontrollers, it reaches ×15 due to the overhead of object creation.\n\n## Zero RAM Allocation on the Hot Path\nThe main parsing loop (parse_line) does not create new objects in memory:\n\n* Comma positions are stored in a pre-allocated bytearray.\n* Message type comparisons (RMC, GGA) and statuses (A, S, W) are performed via integer ASCII codes, not bytes slicing.\n* CRC checking is built into the single-pass scanner without separate calls.\n* GNSS constellation determination uses a table lookup by ASCII codes.\n\nAllocations only happen to save final values (latitude, longitude, speed, course, altitude, time, date) -- this is inevitable, as the user must receive them as independent objects. However, the parsing process itself does not pollute the heap, which is critical for MicroPython with its small RAM and expensive garbage collector.\n## Speed Margin for GPS Applications\n\n| GPS Rate          | Packets/sec | Margin on MicroPython |\n|-------------------|-------------|-----------------------|\n| 1 Гц (standard)   | 1           | ×375                  |\n| 5 Гц (fast)       | 5           | ×75                   |\n| 10 Гц (high rate) | 10          | ×37                   |\n| 20 Гц (extreme)   | 20          | ×18                   |\n\nEven at a rate of 20 Hz, the microcontroller has an x18 CPU margin for other application logic (display rendering, communication, logging).\n\n## Key Architectural Optimizations\n\n1. Single-pass scanner. In a single pass over the string bytes, it calculates the checksum (XOR), finds the payload boundaries (*), and records the indices of all comma separators (,) into a pre-allocated bytearray.\n2. Inline data parsing. The field extraction logic is fully unrolled inside the main loop. All field boundaries are calculated on the fly using the ready indices, which eliminates the overhead of creating stack frames and unnecessary function calls.\n3. Protection against GC freezes. Extracting and converting coordinates, speed, and course into numbers happens \"on the fly\" without generating intermediate string garbage in the heap.\n\n## Supported Messages\n### NMEA 0183 v3.x (Standard)\n\n| Message             | Description                                | Support                  |\n|---------------------|--------------------------------------------|--------------------------|\n| `$GPRMC` / `$GNRMC` | Recommended Minimum Navigation Information | Full parsing             |\n| `$GPGGA` / `$GNGGA` | Global Positioning System Fix Data         | Full parsing             |\n| `$GPGSV`            | Satellites in View                         | Accepted, but not parsed |\n\n### NMEA 0183 v4.10+ (новые Talker ID)\n\n| Talker ID | Constellation          | Example  | Support |\n|-----------|------------------------|----------|---------|\n| `GP`      | GPS (USA)              | `$GPRMC` | +       |\n| `GL`      | GLONASS (Russia)       | `$GLRMC` | +       |\n| `GA`      | Galileo (EU)           | `$GARMC` | +       |\n| `GB`      | BeiDou (China, legacy) | `$GBRMC` | +       |\n| `BD`      | BeiDou (China, new)    | `$BDRMC` | +       |\n| `QZ`      | QZSS (Japan)           | `$QZRMC` | +       |\n| `IR`      | NavIC (Bharat -India)  | `$IRRMC` | +       |\n| `GN`      | Multi-GNSS (mixed)     | `$GNRMC` | +       |\n\n### Constellation Filtering\n```python\nfrom light_nmea import LightNMEA, CST_MASK_GPS, CST_MASK_GALILEO\n\nparser = LightNMEA()\n\n# Accept only GPS and Galileo\nparser.set_cst_filter(CST_MASK_GPS | CST_MASK_GALILEO)\n\n# Accept all constellations (default)\nparser.set_cst_filter(CST_MASK_ALL)\n```\n\n## Installation\n### Installing Competitors for Benchmarks\nCPython (PC, virtual environment recommended)\n```console\npython -m venv .venv\nsource .venv/bin/activate  # Linux/Mac\n# .venv\\Scripts\\activate   # Windows\n```\n#### Install all competitors with a single command:\n```console\npip install pynmea2 pynmeagps\n```\n#### micropyGPS is not on PyPI -- install from GitHub\n\n```console\npip install git+https://github.com/Knio/micropyGPS.git\n```\n#### Alternative (if git is not installed):\n```console\nwget https://raw.githubusercontent.com/Knio/micropyGPS/master/micropyGPS.py\n```\n### MicroPython\nSimply copy the light_nmea folder with the files nmea0183_parser.py, nmea0183_stats.py, nmea0183_stream.py to the root of your microcontroller's filesystem.\nThere are no dependencies - the library uses only standard modules (gc, time, sys).\n\n## Quick Start\n### Basic Usage (Single Packet)\n```python\nfrom light_nmea import LightNMEA\n\n# Initialize the parser\nparser = LightNMEA(trust_gga_fix=True)\n\n# Raw byte string from UART or log file\nraw_sentence = b\"$GNRMC,091530,A,5575.2057,N,03762.6130,E,15.0,120.5,220626,,,A*7F\"\n\n# Parse the string\nif parser.parse_line(raw_sentence):\n    if parser.valid:\n        print(\"Latitude: \", parser.latitude)\n        print(\"Longitude:\", parser.longitude)\n        print(\"Speed:    \", parser.speed, \"km/h\")\n        print(\"Time:     \", parser.time.decode('ascii'))\n```\n\n### Packet Stream Usage\n```python\nfrom light_nmea import LightNMEA\n\nparser = LightNMEA(trust_gga_fix=True)\n\n# Simulate a data stream from a log or UART\nstream_packets = (\n    b\"$GNRMC,091530,A,5575.2057,N,03762.6130,E,15.0,120.5,220626,,,A*7F\",\n    b\"$GNGGA,091530,5575.2057,N,03762.6130,E,1,12,1.0,156.3,M,0.0,M,,*42\"\n)\n\nfor raw_sentence in stream_packets:\n    if parser.parse_line(raw_sentence):\n        print(\"--- Packet processed successfully ---\")\n        print(\"Fix status: \", parser.valid)\n        print(\"UTC Time:   \", parser.time.decode('ascii') if parser.time else \"None\")\n        \n        if parser.has_coordinates():\n            print(\"Coordinates:\", parser.latitude, \",\", parser.longitude)\n            \n        if parser.has_navigation():\n            print(\"2D Nav:     \", parser.speed, \"km/h,\", parser.course, \"deg.\")\n            \n        if parser.has_3d_fix():\n            print(\"3D Fix:     \", parser.altitude, \"meters\")\n            \n        print(\"Satellites: \", parser.satellites)\n```\n\n### MicroPython Example (RP2040 + UART)\n```python\nimport gc\nimport time\nfrom machine import UART, Pin, RTC\nfrom light_nmea import LightNMEA\n\n# Configure UART\nuart = UART(1, baudrate=9600, rx=Pin(5), rxbuf=512)\nparser = LightNMEA(trust_gga_fix=True)\nrtc = RTC()\n\n# Critical for stable MCU operation:\ngc.threshold(8192)  # Auto GC when memory is low\n\ngc_counter = 0\nrtc_synced = False\n\nwhile True:\n    if uart.any():\n        raw_line = uart.readline()\n        if raw_line and len(raw_line) \u003e 10:\n            if parser.parse_line(raw_line):\n                if parser.has_coordinates() and not rtc_synced:\n                    # Sync RTC on first fix\n                    t = parser.time.decode()\n                    d = parser.date.decode()\n                    rtc.datetime((\n                        # Date format is DDMMYY, hence + 2000!\n                        2000 + int(d[4:6]), int(d[2:4]), int(d[0:2]), 0,\n                        int(t[0:2]), int(t[2:4]), int(t[4:6]), 0\n                    ))\n                    rtc_synced = True\n                \n                # Periodic GC every 500 packets\n                gc_counter += 1\n                if gc_counter \u003e= 500:\n                    gc.collect()\n                    gc_counter = 0\n    \n    time.sleep_ms(10)\n```\n\n## API Reference\n### Properties\n* parser.valid (bool): Fix validity flag (True = fix acquired, False = no fix).\n* parser.latitude (float | None): Latitude in degrees (WGS84, range -90.0 to +90.0).\n* parser.longitude (float | None): Longitude in degrees (WGS84, range -180.0 to +180.0).\n* parser.speed (float | None): Movement speed. Automatically converted to km/h.\n* parser.course (float | None): True course (track angle) in degrees relative to geographic north (0.0 - 359.9).\n* parser.altitude (float | None): Altitude above mean sea level (geoid) in meters.\n* parser.satellites (int): Number of satellites used in the solution.\n* parser.time (bytes): UTC time bytes (format HHMMSS).\n* parser.date (bytes): UTC date bytes (format DDMMYY).\n\n### State Check Methods\n* parser.has_coordinates() -\u003e bool: True if both latitude and longitude are parsed and not None.\n* parser.has_navigation() -\u003e bool: True if valid coordinates are available, along with speed and course parameters (full 2D fix).\n* parser.has_3d_fix() -\u003e bool: True if altitude above sea level is available in addition to 2D coordinates (full 3D fix from GGA).\n\n### Memory Monitoring (GPSStats Class)\nTo monitor memory usage, the stats.get_memory_usage() -\u003e float method is used, which returns the amount of occupied kilobytes.\n\n* Under MicroPython, it automatically calls gc.collect() to get the actual heap size.\n* Under Windows and Linux, it queries operating system interfaces without using external libraries.\n\n## Project Directory Structure\nWhen transferring the library to a microcontroller, only the light_nmea folder is required. The other scripts are used for testing and benchmarking on a PC.\n\n```console\nlight-nmea-mp/\n|-- LICENSE                  # GNU GPL v3.0 License\n|-- README_RU.md             # Project documentation (Russian)\n|-- README.md                # Project documentation (English)\n|\n|-- light_nmea/              # Isolated parser package (copy to MCU)\n|   |-- __init__.py          # Python package marker\n|   |-- nmea0183_parser.py   # Parser core\n|   |-- nmea0183_stats.py    # Statistics and RAM measurement module\n|   +-- nmea0183_stream.py   # UART reader with ring buffer\n|\n|-- Entry points\n|   |-- main.py              # Stress test for 1,000,000 packets (CPython)\n|   +-- mpy_main.py          # Main script for RP2040\n|\n|-- Tests\n|   |-- test_leak.py         # Memory leak check script\n|   |-- uart_test.py         # UART test with a real GPS module\n|   +-- run_tests.sh         # Automatic bash script for running tests\n|\n|-- Benchmarks (comparison with competitors)\n|   |-- benchmark/           # Benchmark folder\n|   |   |-- benchmark_pynmea2.py     # Comparison with pynmea2 (CPython)\n|   |   |-- benchmark_pynmeagps.py   # Comparison with pynmeagps (CPython)\n|   |   |-- benchmark_micropygps.py  # Comparison with micropyGPS (MicroPython/CPython)\n|   |   |-- benchmark_adafruitgps.py # Comparison with adafruit_gps (CPython)\n|   |   |-- bench_utils.py   \n|   |   |-- busio.py         # Mock module for CPython\n|   |   |-- micropython.py   # Mock module for CPython\n|   |   +-- readme.txt       # Benchmark description\n|   +-- nav_gen.py           # Test NMEA packet generator\n|\n+-- Test dependencies\n    +-- micropyGPS.py        # Local copy of micropyGPS for testing\n    +-- adafruit_gps.py      # Local copy of adafruit_gps for testing\n```\n\n## NMEAStream Module (nmea0183_stream.py)\nThe nmea0183_stream module implements non-blocking UART reading with a ring buffer and automatic extraction of complete NMEA packets. It solves the main problem when working with GPS modules on microcontrollers: packet loss due to UART buffer overflow.\n### Purpose\n\n* Reading bytes from UART without blocking the main application loop\n* Accumulating data in a fixed-size ring buffer\n* Extracting complete packets (from $ to \\r\\n) with built-in CRC validation\n* Passing ready packets to the LightNMEA parser\n* Cooperative operation with time.sleep_ms() and gc.collect()\n\n## NMEAStream Class (UART Streaming)\nNMEAStream(uart, buffer_size=512): Constructor. Accepts a machine.UART object and the ring buffer size in bytes (default is 512).\n### Methods\n\n* read_available() -\u003e int: Reads all available bytes from the hardware UART buffer into the ring buffer. Does not block execution. Returns the number of bytes read.\n* get_packet() -\u003e bytes | None: Extracts the next complete and valid NMEA packet (with CRC check) from the buffer. Returns bytes or None if the packet is incomplete or corrupted.\n* buffer_usage() -\u003e int: Returns the current number of bytes occupied in the ring buffer (from 0 to buffer_size). Useful for overflow monitoring.\n* reset() -\u003e None: Force clears the ring buffer and resets the frame search state. Used in case of critical overflow.\n\n## Authorship and License\nCopyright 2026 Roman Shevchik\n### License\nGNU General Public License v3.0 (GPL-3.0).\nFree use, modification, and distribution of the code are permitted provided that the source code remains open (copyleft). The license protects users from patent trolls, prohibits the use of the code in closed hardware, and requires preserving notices of original authorship in all modified versions of the files. Any derivative product using this code must be distributed under the same GPL-3.0 license.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctaprog7%2Flight-nmea-micropython","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foctaprog7%2Flight-nmea-micropython","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctaprog7%2Flight-nmea-micropython/lists"}