circuitpython-caveble


Namecircuitpython-caveble JSON
Version 1.1.2 PyPI version JSON
download
home_page
SummaryCave Surveying Bluetooth Protocol
upload_time2023-09-16 14:41:45
maintainer
docs_urlNone
author
requires_python
licenseMIT
keywords adafruit blinka circuitpython micropython disto distox bluetooth cave survey caveble
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Introduction
============

.. image:: https://readthedocs.org/projects/circuitpython-caveble/badge/?version=latest
    :target: https://circuitpython-caveble.readthedocs.io/
    :alt: Documentation Status


.. image:: https://github.com/furbrain/CircuitPython_CaveBLE/workflows/Build%20CI/badge.svg
    :target: https://github.com/furbrain/CircuitPython_CaveBLE/actions
    :alt: Build Status


.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
    :target: https://github.com/psf/black
    :alt: Code Style: Black

Cave Surveying Bluetooth Protocol - a protocol for communicating with
paperless cave surveying tools e.g. `TopoDroid <https://github.com/marcocorvi/topodroid>`_ and
`SexyTopo <https://github.com/richsmith/sexytopo>`_. This protocol was developed for use with
the `Shetland Attack Pony 6 <https://www.shetlandattackpony.co.uk/>`_, but is presented free to use
for anyone who wishes to use it for their cave surveying device.

Protocol
========

In the discussion below, 'instrument' means the device measuring the cave, 'surveyor' means the device
(likely a phone or tablet) receiving the data from the instrument. Description of "read" or "write" is from the
perspective of the surveyor.

This protocol uses a `SurveyProtocolService` on UUID ``137c4435-8a64-4bcb-93f1-3792c6bdc965``.
It has three characteristics:

  * Name characteristis UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc966``. This characteristic is read
    only and is a simple string indicating which protocol is being used. Currently this is hardcoded to "SAP6"
  * Leg characteristic - UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc968``. This characteristic is read only and will
    notify each time a shot is taken. It is a sequence of 17 bytes and is little-endian:

      * Byte 0: Sequence bit. This will alternate between 0 and 1 for successive legs. The surveyor
        must respond with an appropriate ACK (see later), otherwise the instrument will resend after 5
        seconds.
      * Bytes 1-4: Azimuth in degrees as a float.
      * Bytes 5-8: Inclination in degrees as a float.
      * Bytes 9-12: Roll in degrees as a float.
      * Bytes 13-16: Distance in metres as a float.

  * Command characteristic - UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc967``. Write only, surveyor
    can send a single byte to the instrument. Currently defined bytes:

      * ``0x55`` (ACK0): Signals that the surveyor has received a leg with sequence byte 0
      * ``0x56`` (ACK1): Signals that the surveyor has received a leg with sequence byte 1
      * ``0x31`` (START_CAL): Instrument should enter calibration mode
      * ``0x30`` (STOP_CAL): Instrument should leave calibration mode
      * ``0x36`` (LASER_ON): Instrument should turn the laser on
      * ``0x37`` (LASER_OFF): Instrument should turn the laser off
      * ``0x34`` (DEVICE_OFF): Instrument should turn off
      * ``0x38`` (TAKE_SHOT): Instrument should take a reading

    All the above constants are defined in both CircuitPython and Kotlin code

Usage
=====

The software in this repository simplifies use of the above protocol.

CircuitPython
-------------

The CircuitPython code manages the whole notification and responds to ACKs as per the protocol above. It keeps
a queue of legs that have not yet been sent if communication with the surveyor is interrupted and sends these once
communication has been re-established.

Initialising
............

.. code-block:: python

    from adafruit_ble import BLERadio
    from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
    import caveble

    ble = BLERadio()
    ble.name = "SAP6_AB"
    survey_protocol = caveble.SurveyProtocolService()
    advertisement = ProvideServicesAdvertisement(survey_protocol)
    ble.start_advertising(advertisement)

You will need to either call `SurveyProtocolService.poll` repeatedly, or create `SurveyProtocolService.background_task`
as an asyncio task.


Sending data using poll
.......................

.. code-block:: python

    while True:
        survey_protocol.poll()
        # if leg available to send..
            survey_protocol.send_data(azimuth, inclination, distance, roll)
        time.sleep(0.5)

Sending data asynchronously
...........................

.. code-block:: python

    asyncio.create_task(survey_protocol.background_task())
    while True:
        await leg_ready_event
        survey_protocol.send_data(azimuth, inclination, distance, roll)


Receiving commands using poll
.............................

.. code-block:: python

    while True:
        message = survey_protocol.poll()
        if message is not None:
            # do something with message
            # will not receive ACK0 or ACK1 - these are dealt with by `SurveyProtocolService`
        time.sleep(0.5)

Receiving commands asynchronously
.................................

.. code-block:: python

    async def callback(message: int):
        #process the message

    asyncio.create_task(survey_protocol.background_task(callback))
    while True:
        await leg_ready_event
        survey_protocol.send_data(azimuth, inclination, distance, roll)


Kotlin/Java (Android)
---------------------

You can use ``CaveBLE.kt`` in your code - simply change the package to something appropriate on line one. Note Kotlin
is fully compatible with Java and AndroidStudio comfortably uses these files interchangeably in the same project.

To use the device, you must create a ``CaveBLE`` object. you will need to pass in a bluetooth device object, a context,
a leg callback and an optional status callback.
The leg callback will be called whenever a new leg is received
The status callback will be called whenever the device connects or disconnects

Sample Java code
......................

.. code-block:: java

    package xxx.xxx.xxx.xxx;

    import android.bluetooth.BluetoothDevice;
    import android.content.Context;
    import xxx.xxx.xxx.CaveBLE;

    import kotlin.Unit;

    public class SAP6Communicator extends Communicator {

        private final CaveBLE caveBLE;


        public SAP6Communicator(Context context, BluetoothDevice bluetoothDevice) {
            this.caveBLE = new CaveBLE(bluetoothDevice, context, this::legCallback, this::statusCallback);
        }

        public boolean isConnected() {
            return caveBLE.isConnected();
        }

        public void requestConnect() {
            caveBLE.connect();
        }

        public void requestDisconnect() {
            caveBLE.disconnect();
        }

        public void laserOn() {
            caveBLE.laserOn()
        }

        // other commands have similar functions

        public Unit legCallback(float azimuth, float inclination, float roll, float distance) {
            // code to respond to a leg being received here.
            return Unit.INSTANCE; // you must return Unit.INSTANCE for callbacks to Kotlin code
        }

        public Unit statusCallback(int status, String msg) {
            switch (status) {
                case CaveBLE.CONNECTED:
                    // code to run when device connects here
                    break;
                case CaveBLE.DISCONNECTED:
                    Log.device("Disconnected");
                    // code to run when device disconnects here
                    break;
                case CaveBLE.CONNECTION_FAILED:
                    Log.device("Communication error: "+msg);
            }
            return Unit.INSTANCE;
        }
    }


Dependencies
=============
This driver depends on:

* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
* `Adafruit BLE <https://github.com/adafruit/Adafruit_CircuitPython_BLE>`_

Please ensure all dependencies are available on the CircuitPython filesystem.
This is easily achieved by downloading
`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
or individual libraries can be installed using
`circup <https://github.com/adafruit/circup>`_.


Installing from PyPI
====================

On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
PyPI <https://pypi.org/project/circuitpython-caveble/>`_.
To install for current user:

.. code-block:: shell

    pip3 install circuitpython-caveble

To install system-wide (this may be required in some cases):

.. code-block:: shell

    sudo pip3 install circuitpython-caveble

To install in a virtual environment in your current project:

.. code-block:: shell

    mkdir project-name && cd project-name
    python3 -m venv .venv
    source .env/bin/activate
    pip3 install circuitpython-caveble

Installing to a Connected CircuitPython Device with Circup
==========================================================

Make sure that you have ``circup`` installed in your Python environment.
Install it with the following command if necessary:

.. code-block:: shell

    pip3 install circup

With ``circup`` installed and your CircuitPython device connected use the
following command to install:

.. code-block:: shell

    circup install caveble

Or the following command to update an existing version:

.. code-block:: shell

    circup update

Full Usage Example
==================

.. code-block:: python

    import time

    import board
    import keypad
    from adafruit_ble import BLERadio
    from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
    import caveble

    ble = BLERadio()
    ble.name = "SAP6_AB"
    print(ble.name)
    survey_protocol = caveble.SurveyProtocolService()
    advertisement = ProvideServicesAdvertisement(survey_protocol)
    ble.start_advertising(advertisement)


    KEY_PINS = (board.D5, board.D9)
    keys = keypad.Keys(KEY_PINS, value_when_pressed=False, pull=True)

    compass = 0
    clino = 0
    distance = 5
    while True:
        event = keys.events.get()
        if event:
            key_number = event.key_number
            if event.pressed:
                if key_number == 0:
                    # change the values to send
                    compass = (compass + 10.5) % 360
                    clino += 5.5
                    if clino > 90:
                        clino -= 180
                    distance = (distance + 3.4) % 10000
                    print(compass, clino, distance)
                if key_number == 1:
                    survey_protocol.send_data(compass, clino, distance)
                    print("Data sent")
        message = survey_protocol.poll()
        if message:
            print(f"Message received: {message}")
        time.sleep(0.03)

Documentation
=============
API documentation for this library can be found on `Read the Docs <https://circuitpython-caveble.readthedocs.io/>`_.

For information on building library documentation, please check out
`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.

Contributing
============

Contributions are welcome! Please read our `Code of Conduct
<https://github.com/furbrain/CircuitPython_CaveBLE/blob/HEAD/CODE_OF_CONDUCT.md>`_
before contributing to help this project stay welcoming.

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "circuitpython-caveble",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "adafruit,blinka,circuitpython,micropython,disto,distox,bluetooth,cave,survey,caveble",
    "author": "",
    "author_email": "Phil Underwood <beardydoc@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/e0/96/8d9187fc176d924d558257608da226b4b1ae9a7544b304594461569091f2/circuitpython-caveble-1.1.2.tar.gz",
    "platform": null,
    "description": "Introduction\n============\n\n.. image:: https://readthedocs.org/projects/circuitpython-caveble/badge/?version=latest\n    :target: https://circuitpython-caveble.readthedocs.io/\n    :alt: Documentation Status\n\n\n.. image:: https://github.com/furbrain/CircuitPython_CaveBLE/workflows/Build%20CI/badge.svg\n    :target: https://github.com/furbrain/CircuitPython_CaveBLE/actions\n    :alt: Build Status\n\n\n.. image:: https://img.shields.io/badge/code%20style-black-000000.svg\n    :target: https://github.com/psf/black\n    :alt: Code Style: Black\n\nCave Surveying Bluetooth Protocol - a protocol for communicating with\npaperless cave surveying tools e.g. `TopoDroid <https://github.com/marcocorvi/topodroid>`_ and\n`SexyTopo <https://github.com/richsmith/sexytopo>`_. This protocol was developed for use with\nthe `Shetland Attack Pony 6 <https://www.shetlandattackpony.co.uk/>`_, but is presented free to use\nfor anyone who wishes to use it for their cave surveying device.\n\nProtocol\n========\n\nIn the discussion below, 'instrument' means the device measuring the cave, 'surveyor' means the device\n(likely a phone or tablet) receiving the data from the instrument. Description of \"read\" or \"write\" is from the\nperspective of the surveyor.\n\nThis protocol uses a `SurveyProtocolService` on UUID ``137c4435-8a64-4bcb-93f1-3792c6bdc965``.\nIt has three characteristics:\n\n  * Name characteristis UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc966``. This characteristic is read\n    only and is a simple string indicating which protocol is being used. Currently this is hardcoded to \"SAP6\"\n  * Leg characteristic - UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc968``. This characteristic is read only and will\n    notify each time a shot is taken. It is a sequence of 17 bytes and is little-endian:\n\n      * Byte 0: Sequence bit. This will alternate between 0 and 1 for successive legs. The surveyor\n        must respond with an appropriate ACK (see later), otherwise the instrument will resend after 5\n        seconds.\n      * Bytes 1-4: Azimuth in degrees as a float.\n      * Bytes 5-8: Inclination in degrees as a float.\n      * Bytes 9-12: Roll in degrees as a float.\n      * Bytes 13-16: Distance in metres as a float.\n\n  * Command characteristic - UUID: ``137c4435-8a64-4bcb-93f1-3792c6bdc967``. Write only, surveyor\n    can send a single byte to the instrument. Currently defined bytes:\n\n      * ``0x55`` (ACK0): Signals that the surveyor has received a leg with sequence byte 0\n      * ``0x56`` (ACK1): Signals that the surveyor has received a leg with sequence byte 1\n      * ``0x31`` (START_CAL): Instrument should enter calibration mode\n      * ``0x30`` (STOP_CAL): Instrument should leave calibration mode\n      * ``0x36`` (LASER_ON): Instrument should turn the laser on\n      * ``0x37`` (LASER_OFF): Instrument should turn the laser off\n      * ``0x34`` (DEVICE_OFF): Instrument should turn off\n      * ``0x38`` (TAKE_SHOT): Instrument should take a reading\n\n    All the above constants are defined in both CircuitPython and Kotlin code\n\nUsage\n=====\n\nThe software in this repository simplifies use of the above protocol.\n\nCircuitPython\n-------------\n\nThe CircuitPython code manages the whole notification and responds to ACKs as per the protocol above. It keeps\na queue of legs that have not yet been sent if communication with the surveyor is interrupted and sends these once\ncommunication has been re-established.\n\nInitialising\n............\n\n.. code-block:: python\n\n    from adafruit_ble import BLERadio\n    from adafruit_ble.advertising.standard import ProvideServicesAdvertisement\n    import caveble\n\n    ble = BLERadio()\n    ble.name = \"SAP6_AB\"\n    survey_protocol = caveble.SurveyProtocolService()\n    advertisement = ProvideServicesAdvertisement(survey_protocol)\n    ble.start_advertising(advertisement)\n\nYou will need to either call `SurveyProtocolService.poll` repeatedly, or create `SurveyProtocolService.background_task`\nas an asyncio task.\n\n\nSending data using poll\n.......................\n\n.. code-block:: python\n\n    while True:\n        survey_protocol.poll()\n        # if leg available to send..\n            survey_protocol.send_data(azimuth, inclination, distance, roll)\n        time.sleep(0.5)\n\nSending data asynchronously\n...........................\n\n.. code-block:: python\n\n    asyncio.create_task(survey_protocol.background_task())\n    while True:\n        await leg_ready_event\n        survey_protocol.send_data(azimuth, inclination, distance, roll)\n\n\nReceiving commands using poll\n.............................\n\n.. code-block:: python\n\n    while True:\n        message = survey_protocol.poll()\n        if message is not None:\n            # do something with message\n            # will not receive ACK0 or ACK1 - these are dealt with by `SurveyProtocolService`\n        time.sleep(0.5)\n\nReceiving commands asynchronously\n.................................\n\n.. code-block:: python\n\n    async def callback(message: int):\n        #process the message\n\n    asyncio.create_task(survey_protocol.background_task(callback))\n    while True:\n        await leg_ready_event\n        survey_protocol.send_data(azimuth, inclination, distance, roll)\n\n\nKotlin/Java (Android)\n---------------------\n\nYou can use ``CaveBLE.kt`` in your code - simply change the package to something appropriate on line one. Note Kotlin\nis fully compatible with Java and AndroidStudio comfortably uses these files interchangeably in the same project.\n\nTo use the device, you must create a ``CaveBLE`` object. you will need to pass in a bluetooth device object, a context,\na leg callback and an optional status callback.\nThe leg callback will be called whenever a new leg is received\nThe status callback will be called whenever the device connects or disconnects\n\nSample Java code\n......................\n\n.. code-block:: java\n\n    package xxx.xxx.xxx.xxx;\n\n    import android.bluetooth.BluetoothDevice;\n    import android.content.Context;\n    import xxx.xxx.xxx.CaveBLE;\n\n    import kotlin.Unit;\n\n    public class SAP6Communicator extends Communicator {\n\n        private final CaveBLE caveBLE;\n\n\n        public SAP6Communicator(Context context, BluetoothDevice bluetoothDevice) {\n            this.caveBLE = new CaveBLE(bluetoothDevice, context, this::legCallback, this::statusCallback);\n        }\n\n        public boolean isConnected() {\n            return caveBLE.isConnected();\n        }\n\n        public void requestConnect() {\n            caveBLE.connect();\n        }\n\n        public void requestDisconnect() {\n            caveBLE.disconnect();\n        }\n\n        public void laserOn() {\n            caveBLE.laserOn()\n        }\n\n        // other commands have similar functions\n\n        public Unit legCallback(float azimuth, float inclination, float roll, float distance) {\n            // code to respond to a leg being received here.\n            return Unit.INSTANCE; // you must return Unit.INSTANCE for callbacks to Kotlin code\n        }\n\n        public Unit statusCallback(int status, String msg) {\n            switch (status) {\n                case CaveBLE.CONNECTED:\n                    // code to run when device connects here\n                    break;\n                case CaveBLE.DISCONNECTED:\n                    Log.device(\"Disconnected\");\n                    // code to run when device disconnects here\n                    break;\n                case CaveBLE.CONNECTION_FAILED:\n                    Log.device(\"Communication error: \"+msg);\n            }\n            return Unit.INSTANCE;\n        }\n    }\n\n\nDependencies\n=============\nThis driver depends on:\n\n* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_\n* `Adafruit BLE <https://github.com/adafruit/Adafruit_CircuitPython_BLE>`_\n\nPlease ensure all dependencies are available on the CircuitPython filesystem.\nThis is easily achieved by downloading\n`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_\nor individual libraries can be installed using\n`circup <https://github.com/adafruit/circup>`_.\n\n\nInstalling from PyPI\n====================\n\nOn supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from\nPyPI <https://pypi.org/project/circuitpython-caveble/>`_.\nTo install for current user:\n\n.. code-block:: shell\n\n    pip3 install circuitpython-caveble\n\nTo install system-wide (this may be required in some cases):\n\n.. code-block:: shell\n\n    sudo pip3 install circuitpython-caveble\n\nTo install in a virtual environment in your current project:\n\n.. code-block:: shell\n\n    mkdir project-name && cd project-name\n    python3 -m venv .venv\n    source .env/bin/activate\n    pip3 install circuitpython-caveble\n\nInstalling to a Connected CircuitPython Device with Circup\n==========================================================\n\nMake sure that you have ``circup`` installed in your Python environment.\nInstall it with the following command if necessary:\n\n.. code-block:: shell\n\n    pip3 install circup\n\nWith ``circup`` installed and your CircuitPython device connected use the\nfollowing command to install:\n\n.. code-block:: shell\n\n    circup install caveble\n\nOr the following command to update an existing version:\n\n.. code-block:: shell\n\n    circup update\n\nFull Usage Example\n==================\n\n.. code-block:: python\n\n    import time\n\n    import board\n    import keypad\n    from adafruit_ble import BLERadio\n    from adafruit_ble.advertising.standard import ProvideServicesAdvertisement\n    import caveble\n\n    ble = BLERadio()\n    ble.name = \"SAP6_AB\"\n    print(ble.name)\n    survey_protocol = caveble.SurveyProtocolService()\n    advertisement = ProvideServicesAdvertisement(survey_protocol)\n    ble.start_advertising(advertisement)\n\n\n    KEY_PINS = (board.D5, board.D9)\n    keys = keypad.Keys(KEY_PINS, value_when_pressed=False, pull=True)\n\n    compass = 0\n    clino = 0\n    distance = 5\n    while True:\n        event = keys.events.get()\n        if event:\n            key_number = event.key_number\n            if event.pressed:\n                if key_number == 0:\n                    # change the values to send\n                    compass = (compass + 10.5) % 360\n                    clino += 5.5\n                    if clino > 90:\n                        clino -= 180\n                    distance = (distance + 3.4) % 10000\n                    print(compass, clino, distance)\n                if key_number == 1:\n                    survey_protocol.send_data(compass, clino, distance)\n                    print(\"Data sent\")\n        message = survey_protocol.poll()\n        if message:\n            print(f\"Message received: {message}\")\n        time.sleep(0.03)\n\nDocumentation\n=============\nAPI documentation for this library can be found on `Read the Docs <https://circuitpython-caveble.readthedocs.io/>`_.\n\nFor information on building library documentation, please check out\n`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.\n\nContributing\n============\n\nContributions are welcome! Please read our `Code of Conduct\n<https://github.com/furbrain/CircuitPython_CaveBLE/blob/HEAD/CODE_OF_CONDUCT.md>`_\nbefore contributing to help this project stay welcoming.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Cave Surveying Bluetooth Protocol",
    "version": "1.1.2",
    "project_urls": {
        "Homepage": "https://github.com/furbrain/CircuitPython_CaveBLE"
    },
    "split_keywords": [
        "adafruit",
        "blinka",
        "circuitpython",
        "micropython",
        "disto",
        "distox",
        "bluetooth",
        "cave",
        "survey",
        "caveble"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "9962e53cd1ecf1bf116bea3610283f20cb9ab0b1b0d2f8f4eacbe61f7246f2a5",
                "md5": "74d1fa7ee7dc2f8ed19ca986f61968b9",
                "sha256": "22691d3deb2b8be271f2cbd36ab2fcbb6b65a427c6a951a8d443789434b2514b"
            },
            "downloads": -1,
            "filename": "circuitpython_caveble-1.1.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "74d1fa7ee7dc2f8ed19ca986f61968b9",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 8446,
            "upload_time": "2023-09-16T14:41:44",
            "upload_time_iso_8601": "2023-09-16T14:41:44.042676Z",
            "url": "https://files.pythonhosted.org/packages/99/62/e53cd1ecf1bf116bea3610283f20cb9ab0b1b0d2f8f4eacbe61f7246f2a5/circuitpython_caveble-1.1.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e0968d9187fc176d924d558257608da226b4b1ae9a7544b304594461569091f2",
                "md5": "f797df126bcfdfadc55c051061cfb8f6",
                "sha256": "5a20f244f8455195a3c2c995b10af815895e5406f73c19661c1a4f95e642bef2"
            },
            "downloads": -1,
            "filename": "circuitpython-caveble-1.1.2.tar.gz",
            "has_sig": false,
            "md5_digest": "f797df126bcfdfadc55c051061cfb8f6",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 32270,
            "upload_time": "2023-09-16T14:41:45",
            "upload_time_iso_8601": "2023-09-16T14:41:45.904286Z",
            "url": "https://files.pythonhosted.org/packages/e0/96/8d9187fc176d924d558257608da226b4b1ae9a7544b304594461569091f2/circuitpython-caveble-1.1.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-09-16 14:41:45",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "furbrain",
    "github_project": "CircuitPython_CaveBLE",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "lcname": "circuitpython-caveble"
}
        
Elapsed time: 0.11456s