Skip to main content

Cave Surveying Bluetooth Protocol

Project description

Introduction

Documentation Status Build Status Code Style: Black

Cave Surveying Bluetooth Protocol - a protocol for communicating with paperless cave surveying tools e.g. TopoDroid and SexyTopo. This protocol was developed for use with the Shetland Attack Pony 6, 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

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

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

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

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

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

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:

Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading the Adafruit library and driver bundle or individual libraries can be installed using circup.

Installing from PyPI

On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally from PyPI. To install for current user:

pip3 install circuitpython-caveble

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

sudo pip3 install circuitpython-caveble

To install in a virtual environment in your current project:

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:

pip3 install circup

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

circup install caveble

Or the following command to update an existing version:

circup update

Full Usage Example

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.

For information on building library documentation, please check out this guide.

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

circuitpython-caveble-1.1.2.tar.gz (32.3 kB view hashes)

Uploaded Source

Built Distribution

circuitpython_caveble-1.1.2-py3-none-any.whl (8.4 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page