草庐IT

Android BLE 蓝牙开发,连接蓝牙设备进行通讯

Zinyan 2023-03-28 原文

1. 介绍

本篇主要基于 Android 官方的低功耗蓝牙连接服务。

讲解如何通过 UUID 连接蓝牙设备。如果你针对 GATT 服务不太了解。那么这篇应该能够稍微帮助到你。

官方文档地址:https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le?hl=zh_cn#connect

2. 概念

如果是老用户了,那么就应该知道曾经蓝牙设备是一个高耗电的部件。根本不可能长时间开启。而在蓝牙4.0版本之后,蓝牙的通讯,耗电,抗干扰都得到了显著提升。同时蓝牙成本也得到了降低。

然后才有了我们现在的各种穿戴设备例如手环,蓝牙耳机,蓝牙电子秤,蓝牙音箱等等的爆发。

同时,其他工业或者外置设备也都开始大量支持蓝牙通讯。因为能耗和成本降低了。

针对低功耗蓝牙通讯,Android 4.3(API 18)开始引入了 BLE 库。我们可以直接使用 Android SDK 中的蓝牙 BLE 库,而不用额外导入依赖库。

以前开发蓝牙通讯,还需要实现蓝牙配对。需要主动跳转到手机设置界面进行PIN码配对,然后配对通过之后才能进行蓝牙链接。

而使用BLE库,我们可以直接通过蓝牙设备的UUID进行连接(通过GATT服务),在当前应用内就能直接连接了。而不用通过系统设置。

市面上的各种手环的自动匹配链接,电子秤的自动连接等等都是通过GATT进行通讯和链接的。

2.1 术语

  • GATT:全称为:Generic Attribute Profile,翻译为:通用属性配置文件。GATT 配置文件是一种通用规范,内容针对在 BLE 链路上发送和接收称为“属性ATT”的简短数据片段。目前所有低功耗应用配置文件均以 GATT 为基础。
  • ATT:全称为:Attribute Protocol,翻译为:属性协议。它是 GATT 的构建基础,二者的关系也被称为 GATT/ATT。每个属性均由通用唯一标识符 (UUID) 进行唯一标识,后者是用于对信息进行唯一标识的字符串 ID 的 128 位标准化格式。由 ATT 传输的属性采用特征和服务格式。
  • 特征 Characteristic: 特征包含一个值和 0 至多个描述特征值的描述符。您可将特征理解为类型,后者与类类似。
  • 描述符:描述符是描述特征值的已定义属性。例如,描述符可指定人类可读的描述、特征值的可接受范围或特定于特征值的度量单位。
  • Service — 服务是一系列特征。例如,您可能拥有名为“心率监测器”的服务,其中包括“心率测量”等特征。
以上术语的介绍来源于Android官网

2.2 通讯过程

假如我们有一个蓝牙外置设备(Device),然后有一个支持蓝牙的移动设备(Phone)。两者之间的通讯方式步骤是:

  1. Device 开启蓝牙。(通常这些设备都是开机之后,就默认开启蓝牙了)
  2. Phone 开启蓝牙。
  3. Phone 发现 Device。
  4. Phone 与 Device 创建蓝牙连接。
  5. Phone 创建 Gatt 客户端,与 Device Gatt 服务端连接。
  6. Phone 通过 Gatt 服务功能获取 Device 中的消息,并发送消息给 Device 设备。
整个过程就是这样的。下面我也将按照这个通讯过程进行介绍。

3.开发

基于我的使用情况,从无到有的介绍,完整的蓝牙开发配置过程。给大家一个参考

语言主要为 Java

3.1 权限

要在应用中使用蓝牙功能,必须声明 BLUETOOTH 蓝牙权限。需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等。

同时,还需要位置权限。因为蓝牙 LE 信标通常与位置相关联。如果不开启 ACCESS_FINE_LOCATION 权限。那么我们将会无法发现蓝牙设备。

也就是执行蓝牙扫描 API 无法得到任何结果(PS::Logcat 中的错误日志会告诉你,要开启位置权限,否则无法扫描发现蓝牙设备)。

<!-- 蓝牙搜索配对 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 操纵蓝牙的开启-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
其中 android.permission.ACCESS_FINE_LOCATION​ 是高版本API 28 权限。如果要支持更低版本,就需要申请<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />。

如果要执行蓝牙扫描功能,我们需要申请:<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />权限

如果要执行蓝牙链接,开关蓝牙。需要申请:<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />权限

而上面两个权限呢,是在 API 31 上才有效。而低版本就是申请:

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />权限也就够了。

权限配置完毕之后,就是代码开发了。

不管是高版本,还是低版本。将权限都申请可以说最稳妥了。

3.2 检测设备是否支持蓝牙

通常情况下,手机是有蓝牙的。而我们如果在其他 Android 系统的设备中,例如TV,平板,一体机等等。是否有蓝牙还真不能完整保证。

如果不确定的情况下,那么可以通过以下代码检查 BLE 的可用性。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
//不支持蓝牙设备
finish();
} else {
//支持蓝牙设备
}
蓝牙是否开启都不影响检查结果。它检查的是设备是否有蓝牙功能,而不是蓝牙是否启动,下面会介绍如何判断蓝牙是否启动

3.3 开启蓝牙

当我们设备也支持蓝牙了,权限也配置了。下一步就是获取 BluetoothAdapter 对象了。

final BluetoothManager bluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
我们后续控制蓝牙的状态,都是通过该方法实现的。

首先,检测蓝牙是否开启。可以通过isEnabled()方法进行检测:

if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
//开启设备的蓝牙链接
bluetoothAdapter.enable();//开启蓝牙
//动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION ,然后再执行蓝牙扫描
} else {
//动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION,然后再执行蓝牙扫描
}
我们其实可以直接使用bluetoothAdapter.enable()开启蓝牙。当蓝牙没有开启时,我们可以直接开启蓝牙。

这个方法的结果,并不是实时返回的。我们如果要知道蓝牙是否开启,需要监听蓝牙状态的广播才行。下面会介绍广播监听。

PS:这个方法需要android.Manifest.permission.BLUETOOTH_CONNECT 权限才能使用。

官方是建议我们通过Intent让系统设置进行开启蓝牙的。

if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
但是现在startActivityForResult方法已经过时。我们可以使用Launcher来调用:

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
//处理返回结果
}
});
上面的 launcher​需要在Activity​ 的 onCreate 方法中初始化。然后在需要进行蓝牙设置界面启动的地方配置:

Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); //创建一个蓝牙启动的意图
launcher.launch(enableBtIntent);//使用launcer启动这个意图就可以了。
我们如果使用bluetoothAdapter.enable();​时Android Studio出现代码错误警告,可以在该代码使用的方法中添加:@SuppressLint("MissingPermission")注解。

3.4 广播监听

其实这个广播监听,是否需要。根据大家实际情况来定。不一定需要。

首先,创建一个动态广播对象:

public class BluetoothFoundReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
//监听蓝牙状态之后,发送消息
try {
if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
//开始扫描
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
//结束扫描
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
//发现设备,每扫码到一个设备,都会触发一次
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//我们可以得到蓝牙设备
} else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
//蓝牙开关状态

int statue = bluetoothAdapter.getState();
switch (statue) {
case BluetoothAdapter.STATE_OFF:
Log.e(TAG, "蓝牙状态:,蓝牙关闭");
break;
case BluetoothAdapter.STATE_ON:
Log.e(TAG, "蓝牙状态:,蓝牙打开");

break;
case BluetoothAdapter.STATE_TURNING_OFF:
Log.e(TAG, "蓝牙状态:,蓝牙正在关闭");
break;
case BluetoothAdapter.STATE_TURNING_ON:
Log.e(TAG, "蓝牙状态:,蓝牙正在打开");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后进行广播注册:

bluetoothFoundReceiver = new BluetoothFoundReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//连接蓝牙,断开蓝牙
filter.addAction(BluetoothDevice.ACTION_FOUND);//找到设备的广播
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//搜索完成的广播
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//状态改变 配对开始时,配对成功时
registerReceiver(bluetoothFoundReceiver, filter);
注册完毕后,在onDestroy方法中需要注销注册:

@Override
protected void onDestroy() {
if (bluetoothFoundReceiver != null)
unregisterReceiver(bluetoothFoundReceiver); //停止监听
super.onDestroy();
}
其实,我们只需要蓝牙状态的监听就可以了BluetoothAdapter.ACTION_STATE_CHANGED 其他的设备查找,配对。可以不用,因为触发到广播的设备查找效率太低,而且多次重复查找时,还会出现耗时变长。设备无法查找到的情况。

3.5 蓝牙设备查找

官方文档上推荐的查找方式是:

bluetoothAdapter.startLeScan(leScanCallback); //查找
bluetoothAdapter.stopLeScan(leScanCallback); //停止查找
可是现在这个方法也过时了。替换方法是:

BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
//不进行权限验证
ScanCallback callback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();//得到设备
// Log.e(TAG, "发现设备" + device.getName());
}

@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "搜索错误" + errorCode);
}
};
scanner.startScan(callback);
onScanResult方法是一个在子线程触发的回调,我们不能在该方法中直接操作UI对象。

其次,扫描到一个蓝牙设备就会触发一次消息回调。我们可以得到一个BluetoothDevice对象。 也就是说这个方法中会触发多次回调,

所以建议,在扫描到我们的蓝牙设备之后,主动调用scanner.stopScan(callback);停止扫描。

PS:这种查找方式,不会触发蓝牙的遍历广播。我们如果开启广播进行监听设备扫描情况。如果通过startScan方法,广播中不会有回调。

上面是一个通用搜索模式,我们还可以配置自己的过滤条件。例如:

ScanFilter sn =new ScanFilter.Builder().setDeviceName("蓝牙设备的名称").setServiceUuid(ParcelUuid.fromString("我们的设备的Service UUID")).build();
List<ScanFilter> scanFilters=new ArrayList<>();
scanFilters.add(sn);
scanner.startScan(scanFilters, new ScanSettings.Builder().build(),callback);
其中ScanFilter​对象,我们可以配置我们想查找的蓝牙设备的信息。可以是setDeviceName,setServiceUuid,setDeviceAddress,setServiceSolicitationUuid等。

ScanSettings对象是可以定义我们的扫描模式,通过配置该项可以提高扫描效率。

默认情况下,执行的是:SCAN_MODE_LOW_POWER在低功耗模式下执行蓝牙LE扫描。 这是默认的扫描模式,因为它消耗最少的电量。

3.5.1 startDiscovery

如果上面的方法还不满足我们的情况,可以使用:

if (bluetoothAdapter.isDiscovering()) {//是否在扫描
bluetoothAdapter.cancelDiscovery(); //停止扫描
}
//查找蓝牙
bluetoothAdapter.startDiscovery();
我们可以直接使用bluetoothAdapter进行扫描。这个方法触发之后是由系统进行蓝牙扫描。就和我们在手机的设置界面中点击蓝牙扫描一样。

上面的这个方法没有回调,因为所有的蓝牙设备的发现都将通过广播事件进行传递。

需要通过我上面的广播监听介绍的内容。进行实时获取到扫描到的设备。

使用上面的方法有几个缺点:

1.效率慢,耗时很长。

2.重复扫描会失败。不能说是失败了,而是系统会将重复扫描的请求进行阻止,关键的问题在于这个阻止操作是手机厂商定制的。

PS:不管是BluetoothLeScanner​ 还是bluetoothAdapter.startDiscovery() 去查找蓝牙设备。都不建议一直重复扫描。否则会出现无法扫描到设备,没有任何扫描结果等等情况。因为扫描是一个耗时耗电的操作。

3.6 链接Gatt

当我们扫描到了蓝牙设备之后,就会获取到BluetoothDevice​对象。然后我们通过BluetoothDevice​对象创建GATT服务进行后续的蓝牙通讯。

BluetoothDevice device;// 当我们通过扫描得到device对象之后,进行Gatt服务创建
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, gattCallback);
第一个传参context没有什么可以介绍的。

第二个传参autoConnect:是一个boolean值对象,false代表直接连接到蓝牙设备。true代表在蓝牙设备可用时自动连接。

第三个参数BluetoothGattCallback 是Gatt服务的各种回调了。

我们通过gattCallback回调的内容,来得到与蓝牙设备的链接状态,数据通信内容等。

下面来详细介绍下BluetoothGattCallback对象的几个方法。

String SERVICE_UUID="00000-000000-000000-000000";//这个是我要链接的蓝牙设备的ServiceUUID

BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
//GATT的链接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
Log.v(TAG, "连接成功");
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.e(TAG, "连接断开");
} else if (newState == BluetoothProfile.STATE_CONNECTING) {
//TODO 在实际过程中,该方法并没有调用
Log.e(TAG, "连接中....");
}
}
//获取GATT服务发现后的回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "GATT_SUCCESS"); //服务发现
for (BluetoothGattService bluetoothGattService : gatt.getServices()) {
Log.e(TAG, "Service_UUID" + bluetoothGattService.getUuid()); // 我们可以遍历到该蓝牙设备的全部Service对象。然后通过比较Service的UUID,我们可以区分该服务是属于什么业务的
if (SERVICE_UUID.equals(bluetoothGattService.getUuid().toString())) {

for (BluetoothGattCharacteristic characteristic : bluetoothGattService.getCharacteristics()) {
prepareBroadcastDataNotify(gatt, characteristic); //给满足条件的属性配置上消息通知
}
return;//结束循环操作
}
}
} else {
Log.e(TAG, "onServicesDiscovered received: " + status);
}
}

//蓝牙设备发送消息后的自动监听
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// readUUID 是我要链接的蓝牙设备的消息读UUID值,跟通知的特性的UUID比较。这样可以避免其他消息的污染。
if (READ_UUID.equals(characteristic.getUuid().toString())) {
try {
String chara = new String(characteristic.getValue(), "UTF-8");
Log.e(TAG, "消息内容:" + chara);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
};
我们可以通过链接成功和链接断开。来判断我们当前与蓝牙设备的通讯状态。

当我们比对Service​的UUID成功之后, 我们就可以获取Service的Characteristic对象。该对象也就是特征。通过注册特征来实现消息的监听和发送业务。

3.7 注册消息监听-setCharacteristicNotification

@SuppressLint("MissingPermission")
private void prepareBroadcastDataNotify(BluetoothGatt mBluetoothGatt, BluetoothGattCharacteristic characteristic) {
Log.e(TAG, "CharacteristicUUID:" + characteristic.getUuid().toString());
int charaProp = characteristic.getProperties();
//判断属性是否支持消息通知
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
BluetoothGattDescriptor descriptor =
characteristic.getDescriptor(UUID.fromString(UUIDManager.READ_DEDSCRIPTION_UUID));
if (descriptor != null) {
//注册消息通知
mBluetoothGatt.setCharacteristicNotification(characteristic, true);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
在上面的示例中:READ_DEDSCRIPTION_UUID = "00002902-0000-1000-8000-00805f9b34fb" 是固定的,不管你链接什么样的蓝牙设备。

在注册消息监听,都是使用UUID值是00002902-0000-1000-8000-00805f9b34fb进行的。这个是Android系统保留的。用于动态监听的。

你如果不想使用这个动态监听。就需要自己写线程主动去轮询获取到蓝牙设备发送过来的消息了。

到这里,我们其实就能够实现蓝牙设备的实时监听,并得到消息内容了。

3.8 写数据到蓝牙设备中

我们如果想将内容推送到蓝牙设备中,在发现服务的时候onServicesDiscovered 遍历特性中,确保是用于写消息的特性对象后。选择持有该特性,然后通过:

String data ="0x12";
BluetoothGattCharacteristic writeCharact = bluetoothGattService.
getCharacteristic(UUID.fromString(WRITE_UUID));
//查找UUID是写的特性,并检测是否拥有写权限
if (writeCharact == null || writeCharact.getProperties() != BluetoothGattCharacteristic.PROPERTY_WRITE) {
return ;//该特性没有写的权限。所以无法传入
}
// 当数据传递到蓝牙之后
// 会回调BluetoothGattCallback里面的write方法
writeCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
// 将需要传递的数据转为16进制数
writeCharact.setValue(data);
bluetoothGatt.writeCharacteristic(writeCharact);

3.9 关闭连接

当蓝牙通讯结束,或者界面关闭时。我们需要关闭GATT服务,减少资源占用。

if (bluetoothGatt != null) {
bluetoothGatt.close();
bluetoothGatt.disconnect();
bluetoothGatt = null;
}
也可以关闭BluetoothGattCallback 的回调监听:

gattCallback.disConnectBlue();//关闭GATT服务回调监听

4. 小结

到这里蓝牙的链接和读取就结束了。

我们通过bluetoothAdapter 查找到蓝牙设备之后,再通过GATT服务进行蓝牙设备与手机之间的配对。直接比对UUID,而不再需要PIN码进行配对了。

(PS:有些安全性要求比较高的设备,还是会需要主动进行PIN码配对。PIN配队就只能通过系统设备界面中的蓝牙功能项进行操作了。)

通过GATT服务连接成功后。就可以查询该Server下的各种特性了,不同的特性对应了一个功能。有发消息的特性,也有用于收消息的特性。

同时一个蓝牙设备对象,可能有多种服务功能。

如果不想自己写线程变量轮询设备发送过来的消息,就通过注册消息监听。让BLE框架帮我们进行轮询之后,再通知到我们。

有关Android BLE 蓝牙开发,连接蓝牙设备进行通讯的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  4. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  5. ruby - 如何进行排列以有效地定制输出 - 2

    这是一道面试题,我没有答对,但还是很好奇怎么解。你有N个人的大家庭,分别是1,2,3,...,N岁。你想给你的大家庭拍张照片。所有的家庭成员都排成一排。“我是家里的friend,建议家庭成员安排如下:”1岁的家庭成员坐在这一排的最左边。每两个坐在一起的家庭成员的年龄相差不得超过2岁。输入:整数N,1≤N≤55。输出:摄影师可以拍摄的照片数量。示例->输入:4,输出:4符合条件的数组:[1,2,3,4][1,2,4,3][1,3,2,4][1,3,4,2]另一个例子:输入:5输出:6符合条件的数组:[1,2,3,4,5][1,2,3,5,4][1,2,4,3,5][1,2,4,5,3][

  6. ruby - 无法在 60 秒内获得稳定的 Firefox 连接 (127.0.0.1 :7055) - 2

    我使用的是Firefox版本36.0.1和Selenium-Webdrivergem版本2.45.0。我能够创建Firefox实例,但无法使用脚本继续进行进一步的操作无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055)错误。有人能帮帮我吗? 最佳答案 我遇到了同样的问题。降级到firefoxv33后一切正常。您可以找到旧版本here 关于ruby-无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055),我们在StackOverflow上找到一个类

  7. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  8. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  9. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  10. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

    这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

随机推荐