/******************************************************************************* * Copyright (c) 2011, 2016 Eurotech and/or its affiliates * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Eurotech *******************************************************************************/ package org.eclipse.kura.linux.bluetooth.util; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.apache.commons.io.FileUtils; import org.eclipse.kura.KuraErrorCode; import org.eclipse.kura.KuraException; import org.eclipse.kura.bluetooth.BluetoothBeaconData; import org.eclipse.kura.bluetooth.listener.AdvertisingReportRecord; import org.eclipse.kura.bluetooth.listener.BluetoothAdvertisementData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BluetoothUtil { private static final Logger s_logger = LoggerFactory.getLogger(BluetoothUtil.class); private static final ExecutorService s_processExecutor = Executors.newSingleThreadExecutor(); public static final String HCITOOL = "hcitool"; public static final String BTDUMP = "/tmp/BluetoothUtil.btsnoopdump.sh"; private static final String BD_ADDRESS = "BD Address:"; private static final String HCI_VERSION = "HCI Version:"; private static final String HCICONFIG = "hciconfig"; private static final String GATTTOOL = "gatttool"; // Write bluetooth dumping script into /tmp static { try { File f = new File(BTDUMP); FileUtils.writeStringToFile(f, "#!/bin/bash\n" + "set -e\n" + "ADAPTER=$1\n" + "{ hcidump -i $ADAPTER -R -w /dev/fd/3 >/dev/null; } 3>&1", false); f.setExecutable(true); } catch (IOException e) { s_logger.info("Unable to update", e); } } /* * Use hciconfig utility to return information about the bluetooth adapter */ public static Map<String, String> getConfig(String name) throws KuraException { Map<String, String> props = new HashMap<String, String>(); BluetoothSafeProcess proc = null; BufferedReader br = null; StringBuilder sb = null; String[] command = { HCICONFIG, name, "version" }; try { proc = BluetoothProcessUtil.exec(command); // Check Error stream br = new BufferedReader(new InputStreamReader(proc.getErrorStream())); String line = null; while ((line = br.readLine()) != null) { if (line.toLowerCase().contains("command not found")) { throw new KuraException(KuraErrorCode.OPERATION_NOT_SUPPORTED); } else if (line.toLowerCase().contains("no such device")) { throw new KuraException(KuraErrorCode.INTERNAL_ERROR); } } if (br != null) { br.close(); } // Check Input stream br = new BufferedReader(new InputStreamReader(proc.getInputStream())); sb = new StringBuilder(); line = null; while ((line = br.readLine()) != null) { sb.append(line + "\n"); } // TODO: Pull more parameters from hciconfig? String[] results = sb.toString().split("\n"); props.put("leReady", "false"); for (String result : results) { if (result.indexOf(BD_ADDRESS) >= 0) { // Address reported as: // BD Address: xx:xx:xx:xx:xx:xx ACL MTU: xx:xx SCO MTU: xx:x String[] ss = result.split(" "); String address = ""; for (String sss : ss) { if (sss.matches("^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$")) { address = sss; break; } } // String address = result.substring(index + BD_ADDRESS.length()); // String[] tmpAddress = address.split("\\s", 2); // address = tmpAddress[0].trim(); props.put("address", address); s_logger.trace("Bluetooth adapter address set to: {}", address); } if (result.indexOf(HCI_VERSION) >= 0) { // HCI version : 4.0 (0x6) or HCI version : 4.1 (0x7) if (result.indexOf("0x6") >= 0 || result.indexOf("0x7") >= 0) { props.put("leReady", "true"); s_logger.trace("Bluetooth adapter is LE ready"); } } } } catch (Exception e) { s_logger.error("Failed to execute command: {}", command, e); throw new KuraException(KuraErrorCode.INTERNAL_ERROR); } finally { try { if (br != null) { br.close(); } if (proc != null) { proc.destroy(); } } catch (IOException e) { s_logger.error("Error closing read buffer", e); } } return props; } /* * Use hciconfig utility to determine status of bluetooth adapter */ public static boolean isEnabled(String name) { String[] command = { HCICONFIG, name }; BluetoothSafeProcess proc = null; BufferedReader br = null; try { proc = BluetoothProcessUtil.exec(command); br = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line = null; while ((line = br.readLine()) != null) { if (line.contains("UP")) { return true; } if (line.contains("DOWN")) { return false; } } } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } finally { try { if (br != null) { br.close(); } if (proc != null) { proc.destroy(); } } catch (IOException e) { s_logger.error("Error closing read buffer", e); } } return false; } /* * Utility method that allows sending any hciconfig command. The buffered * response is returned in case results are needed. */ public static BufferedReader hciconfigCmd(String name, String cmd) { String[] command = { HCICONFIG, name, cmd }; BluetoothSafeProcess proc = null; BufferedReader br = null; try { proc = BluetoothProcessUtil.exec(command); br = new BufferedReader(new InputStreamReader(proc.getInputStream())); } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } finally { try { if (br != null) { br.close(); } if (proc != null) { proc.destroy(); } } catch (IOException e) { s_logger.error("Error closing read buffer", e); } } return br; } /* * Utility method to send specific kill commands to processes. */ public static void killCmd(String cmd, String signal) { // String[] command = { "pkill", "-" + signal, cmd }; String[] commandPidOf = { "pidof", cmd }; BluetoothSafeProcess proc = null; BufferedReader br = null; try { proc = BluetoothProcessUtil.exec(commandPidOf); proc.waitFor(); br = new BufferedReader(new InputStreamReader(proc.getInputStream())); String pid = br.readLine(); // Check if the pid is not empty if (pid != null) { String[] commandKill = { "kill", "-" + signal, pid }; proc = BluetoothProcessUtil.exec(commandKill); } } catch (IOException e) { s_logger.error("Error executing command: {}", commandPidOf, e); } catch (InterruptedException e) { s_logger.warn("Error executing command: {}", commandPidOf, e); } finally { if (proc != null) { proc.destroy(); } try { if (proc != null) { br.close(); } } catch (IOException e) { s_logger.warn("Error closing process for command: {}", commandPidOf, e); } } } /** * Start an hci dump process for the examination of BLE advertisement packets * * @param name * Name of HCI device (hci0, for example) * @param listener * Listener for receiving btsnoop records * @return BluetoothProcess created */ public static BluetoothProcess btdumpCmd(String name, BTSnoopListener listener) { String[] command = { BTDUMP, name }; BluetoothProcess proc = null; try { s_logger.debug("Command executed : {}", Arrays.toString(command)); proc = execSnoop(command, listener); } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } return proc; } /* * Method to utilize BluetoothProcess and the hcitool utility. These processes run indefinitely, so the * BluetoothProcessListener is used to receive output from the process. */ public static BluetoothProcess hcitoolCmd(String name, String cmd, BluetoothProcessListener listener) { String[] command = { HCITOOL, "-i", name, cmd }; BluetoothProcess proc = null; try { s_logger.debug("Command executed : {}", Arrays.toString(command)); proc = exec(command, listener); } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } return proc; } /* * Method to utilize BluetoothProcess and the hcitool utility. These processes run indefinitely, so the * BluetoothProcessListener is used to receive output from the process. */ public static BluetoothProcess hcitoolCmd(String name, String[] cmd, BluetoothProcessListener listener) { String[] command = new String[3 + cmd.length]; command[0] = HCITOOL; command[1] = "-i"; command[2] = name; for (int i = 0; i < cmd.length; i++) { command[i + 3] = cmd[i]; } BluetoothProcess proc = null; try { s_logger.debug("Command executed : {}", Arrays.toString(command)); proc = exec(command, listener); } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } return proc; } /* * Method to start an interactive session with a remote Bluetooth LE device using the gatttool utility. The * listener is used to receive output from the process. */ public static BluetoothProcess startSession(String adapterName, String address, BluetoothProcessListener listener) { String[] command = { GATTTOOL, "-i", adapterName, "-b", address, "-I" }; BluetoothProcess proc = null; try { proc = exec(command, listener); } catch (Exception e) { s_logger.error("Error executing command: {}", command, e); } return proc; } /* * Method to create a separate thread for the BluetoothProcesses. */ private static BluetoothProcess exec(final String[] cmdArray, final BluetoothProcessListener listener) throws IOException { // Serialize process executions. One at a time so we can consume all streams. Future<BluetoothProcess> futureSafeProcess = s_processExecutor.submit(new Callable<BluetoothProcess>() { @Override public BluetoothProcess call() throws Exception { Thread.currentThread().setName("BluetoothProcessExecutor"); BluetoothProcess bluetoothProcess = new BluetoothProcess(); bluetoothProcess.exec(cmdArray, listener); return bluetoothProcess; } }); try { return futureSafeProcess.get(); } catch (Exception e) { s_logger.error("Error waiting from SafeProcess output", e); throw new IOException(e); } } /* * Method to create a separate thread for the BluetoothProcesses. */ private static BluetoothProcess execSnoop(final String[] cmdArray, final BTSnoopListener listener) throws IOException { // Serialize process executions. One at a time so we can consume all streams. Future<BluetoothProcess> futureSafeProcess = s_processExecutor.submit(new Callable<BluetoothProcess>() { @Override public BluetoothProcess call() throws Exception { Thread.currentThread().setName("BTSnoopProcessExecutor"); BluetoothProcess bluetoothProcess = new BluetoothProcess(); bluetoothProcess.execSnoop(cmdArray, listener); return bluetoothProcess; } }); try { return futureSafeProcess.get(); } catch (Exception e) { s_logger.error("Error waiting from SafeProcess output", e); throw new IOException(e); } } /** * Parse EIR data from a BLE advertising report, * extracting UUID, major and minor number. * * See Bluetooth Core 4.0; 8 EXTENDED INQUIRY RESPONSE DATA FORMAT * * @param b * Array containing EIR data * @param i * Index of first byte of EIR data * @return BeaconInfo or null if no beacon data present */ private static BluetoothBeaconData parseEIRData(byte[] b, int payloadPtr, int len, String companyName) { for (int ptr = payloadPtr; ptr < payloadPtr + len;) { int structSize = b[ptr]; if (structSize == 0) { break; } byte dataType = b[ptr + 1]; if (dataType == (byte) 0xFF) { // Data-Type: Manufacturer-Specific int prefixPtr = ptr + 2; byte[] prefix = new byte[4]; prefix[0] = new Integer(Integer.parseInt(companyName.substring(2, 4), 16)).byteValue(); prefix[1] = new Integer(Integer.parseInt(companyName.substring(0, 2), 16)).byteValue(); prefix[2] = 0x02; prefix[3] = 0x15; if (Arrays.equals(prefix, Arrays.copyOfRange(b, prefixPtr, prefixPtr + prefix.length))) { BluetoothBeaconData bi = new BluetoothBeaconData(); int uuidPtr = ptr + 2 + prefix.length; int majorPtr = uuidPtr + 16; int minorPtr = uuidPtr + 18; bi.uuid = ""; for (byte ub : Arrays.copyOfRange(b, uuidPtr, majorPtr)) { bi.uuid += String.format("%02X", ub); } int majorl = b[majorPtr + 1] & 0xFF; int majorh = b[majorPtr] & 0xFF; int minorl = b[minorPtr + 1] & 0xFF; int minorh = b[minorPtr] & 0xFF; bi.major = majorh << 8 | majorl; bi.minor = minorh << 8 | minorl; bi.txpower = b[minorPtr + 2]; // Can't fill this in from here bi.address = ""; return bi; } } ptr += structSize + 1; } return null; } /** * Check for advertisement out of an HCL LE Advertising Report Event * * See Bluetooth Core 4.0; 7.7.65.2 LE Advertising Report Event * * @param b * @return */ public static BluetoothAdvertisementData parseLEAdvertisement(byte[] b) { BluetoothAdvertisementData btAdData = null; if (b[0] != 0x04 || b[1] != 0x3E) { // Not and Advertisement Packet return btAdData; } // LE Advertisement Subevent Code: 0x02 if (b[3] != 0x02) { // Not a Advertisement Sub Event return btAdData; } // Start building Advertisement Data btAdData = new BluetoothAdvertisementData(); btAdData.setRawData(b); btAdData.setPacketType(b[0]); btAdData.setEventType(b[1]); btAdData.setParameterLength(b[2]); btAdData.setSubEventCode(b[3]); // Number of reports in this advertisement btAdData.setNumberOfReports(b[4]); // Parse each report int ptr = 5; for (int nr = 0; nr < btAdData.getNumberOfReports(); nr++) { AdvertisingReportRecord arr = new AdvertisingReportRecord(); arr.setEventType(b[ptr++]); arr.setAddressType(b[ptr++]); // Extract remote address String address = String.format("%02X:%02X:%02X:%02X:%02X:%02X", b[ptr + 5], b[ptr + 4], b[ptr + 3], b[ptr + 2], b[ptr + 1], b[ptr + 0]); arr.setAddress(address); ptr += 6; int arrDataLength = b[ptr++]; arr.setLength(b[ptr++]); byte[] arrData = new byte[arrDataLength]; System.arraycopy(b, ptr, arrData, 0, arrDataLength); arr.setReportData(arrData); btAdData.addReportRecord(arr); ptr += arrDataLength; } return btAdData; } /** * Parse BLE beacons out of an HCL LE Advertising Report Event * * See Bluetooth Core 4.0; 7.7.65.2 LE Advertising Report Event * * @param b * @return */ public static List<BluetoothBeaconData> parseLEAdvertisingReport(byte[] b, String companyName) { List<BluetoothBeaconData> results = new LinkedList<BluetoothBeaconData>(); // Packet Type: Event OR Event Type: LE Advertisement Report if (b[0] != 0x04 || b[1] != 0x3E) { return results; } // LE Advertisement Subevent Code: 0x02 if (b[3] != 0x02) { return results; } int numReports = b[4]; int ptr = 5; for (int i = 0; i < numReports; i++) { ptr++; ptr++; // Extract remote address String address = String.format("%02X:%02X:%02X:%02X:%02X:%02X", b[ptr + 5], b[ptr + 4], b[ptr + 3], b[ptr + 2], b[ptr + 1], b[ptr + 0]); ptr += 6; int len = b[ptr++]; BluetoothBeaconData bi = parseEIRData(b, ptr, len, companyName); if (bi != null) { bi.address = address; bi.rssi = b[ptr + len]; results.add(bi); } ptr += len; } return results; } }