Skip to content

Commit c31a435

Browse files
committed
Vectronix Terrapin-X laser rangefinder protocol
1 parent 1d830c4 commit c31a435

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

app/src/main/java/com/platypii/baseline/lasers/rangefinder/RangefinderService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class RangefinderService {
3030
private final BleProtocol protocols[] = {
3131
new ATNProtocol(),
3232
new SigSauerProtocol(),
33+
new TerrapinProtocol(),
3334
new UineyeProtocol()
3435
};
3536

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.platypii.baseline.lasers.rangefinder;
2+
3+
import com.platypii.baseline.bluetooth.BleException;
4+
import com.platypii.baseline.bluetooth.BleProtocol;
5+
import com.platypii.baseline.bluetooth.BluetoothUtil;
6+
import com.platypii.baseline.lasers.LaserMeasurement;
7+
import com.platypii.baseline.util.Exceptions;
8+
9+
import android.bluetooth.le.ScanRecord;
10+
import android.os.ParcelUuid;
11+
import android.util.Log;
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import com.welie.blessed.BluetoothPeripheral;
15+
import com.welie.blessed.WriteType;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.UUID;
19+
import org.greenrobot.eventbus.EventBus;
20+
21+
import static com.platypii.baseline.bluetooth.BluetoothUtil.byteArrayToHex;
22+
import static com.platypii.baseline.bluetooth.BluetoothUtil.bytesToShort;
23+
import static com.platypii.baseline.bluetooth.BluetoothUtil.toManufacturerString;
24+
25+
/**
26+
* This class contains ids, commands, and decoders for Vectronix Terrapin-X laser rangefinders.
27+
*/
28+
class TerrapinProtocol extends BleProtocol {
29+
private static final String TAG = "TerrapinProtocol";
30+
31+
// Manufacturer ID
32+
private static final int manufacturerId1 = 1164;
33+
private static final byte[] manufacturerData1 = {1, -96, -1, -1, -1, -1, 0}; // 01-a0-ff-ff-ff-ff-00
34+
35+
// Terrapin service
36+
private static final UUID terrapinService = UUID.fromString("85920000-0338-4b83-ae4a-ac1d217adb03");
37+
// Terrapin characteristic: read, indicate
38+
private static final UUID terrapinCharacteristic1 = UUID.fromString("85920100-0338-4b83-ae4a-ac1d217adb03");
39+
// Terrapin characteristic: notify, write
40+
private static final UUID terrapinCharacteristic2 = UUID.fromString("85920200-0338-4b83-ae4a-ac1d217adb03");
41+
42+
private static final String factoryModeSecretKey = "B6987833";
43+
private static final String packetTypeCommand = "0000";
44+
private static final String packetTypeData = "0300";
45+
private static final String packetTypeAck = "0400";
46+
private static final String packetTypeNack = "0500";
47+
48+
// Say hello to laser
49+
private static final byte[] commandStartMeasurement = {1, 16}; // 0110
50+
51+
@Override
52+
public void onServicesDiscovered(@NonNull BluetoothPeripheral peripheral) {
53+
try {
54+
// Request rangefinder service
55+
Log.i(TAG, "app -> rf: subscribe");
56+
peripheral.setNotify(terrapinService, terrapinCharacteristic2, true);
57+
sendHello(peripheral);
58+
readRangefinder(peripheral);
59+
} catch (Throwable e) {
60+
Log.e(TAG, "rangefinder handshake exception", e);
61+
}
62+
}
63+
64+
@Override
65+
public void processBytes(@NonNull BluetoothPeripheral peripheral, @NonNull byte[] value) {
66+
final String hex = byteArrayToHex(value);
67+
if (!hex.startsWith("7e-") || !hex.endsWith("7e")) {
68+
Log.w(TAG, "rf -> app: invalid command " + hex);
69+
return;
70+
}
71+
Log.i(TAG, "rf -> app: unknown " + hex);
72+
}
73+
74+
private void readRangefinder(@NonNull BluetoothPeripheral peripheral) {
75+
Log.i(TAG, "app -> rf: read");
76+
peripheral.readCharacteristic(terrapinService, terrapinCharacteristic1);
77+
}
78+
79+
private void sendHello(@NonNull BluetoothPeripheral peripheral) {
80+
Log.d(TAG, "app -> rf: hello");
81+
BluetoothUtil.sleep(5000);
82+
peripheral.writeCharacteristic(terrapinService, terrapinCharacteristic2, commandStartMeasurement, WriteType.WITH_RESPONSE);
83+
}
84+
85+
private void processMeasurement(@NonNull byte[] value) {
86+
Log.d(TAG, "rf -> app: measure " + byteArrayToHex(value));
87+
88+
final double units; // unit multiplier
89+
if (value[21] == 1) {
90+
units = 1; // meters
91+
} else if (value[21] == 2) {
92+
units = 0.9144; // yards
93+
} else if (value[21] == 3) {
94+
units = 0.3048; // feet
95+
} else {
96+
Exceptions.report(new IllegalStateException("Unexpected units value from uineye " + value[21]));
97+
units = 0;
98+
}
99+
final double pitch = bytesToShort(value[3], value[4]) * 0.1 * units; // degrees
100+
// final double total = Util.bytesToShort(value[5], value[6]) * 0.1 * units; // meters
101+
double vert = bytesToShort(value[7], value[8]) * 0.1 * units; // meters
102+
double horiz = bytesToShort(value[9], value[10]) * 0.1 * units; // meters
103+
// double bearing = (value[22] & 0xff) * 360.0 / 256.0; // degrees
104+
if (pitch < 0 && vert > 0) {
105+
vert = -vert;
106+
}
107+
108+
final LaserMeasurement meas = new LaserMeasurement(horiz, vert);
109+
Log.i(TAG, "rf -> app: measure " + meas);
110+
EventBus.getDefault().post(meas);
111+
}
112+
113+
/**
114+
* Return true iff a bluetooth scan result looks like a rangefinder
115+
*/
116+
@Override
117+
public boolean canParse(@NonNull BluetoothPeripheral peripheral, @Nullable ScanRecord record) {
118+
final String deviceName = peripheral.getName();
119+
if (record != null && Arrays.equals(record.getManufacturerSpecificData(manufacturerId1), manufacturerData1)) {
120+
return true; // Manufacturer match (kenny's laser)
121+
} else if (
122+
(record != null && hasRangefinderService(record))
123+
|| deviceName.startsWith("FastM")
124+
|| deviceName.startsWith("Terrapin")) {
125+
// Send manufacturer data to firebase
126+
final String mfg = toManufacturerString(record);
127+
Exceptions.report(new BleException("Terrapin laser unknown mfg data: " + deviceName + " " + mfg));
128+
return true;
129+
} else {
130+
return false;
131+
}
132+
}
133+
134+
private boolean hasRangefinderService(@NonNull ScanRecord record) {
135+
final List<ParcelUuid> uuids = record.getServiceUuids();
136+
return uuids != null && uuids.contains(new ParcelUuid(terrapinService));
137+
}
138+
}

0 commit comments

Comments
 (0)