Repository: lunan0320/Pioneer
Branch: main
Commit: 107369941b2e
Files: 258
Total size: 672.7 KB
Directory structure:
gitextract_i2vhfrty/
├── Android_apk/
│ └── appAndroid.apk
├── Android_app/
│ ├── app/
│ │ ├── .gitignore
│ │ ├── build.gradle
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ ├── androidTest/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── ABC/
│ │ │ └── pioneer/
│ │ │ └── app/
│ │ │ └── ExampleInstrumentedTest.java
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── assets/
│ │ │ │ ├── Pioneer.keystore
│ │ │ │ └── server.p12
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── ABC/
│ │ │ │ └── pioneer/
│ │ │ │ └── app/
│ │ │ │ ├── AppDelegate.java
│ │ │ │ ├── LoginActivity.java
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── RegisterActivity.java
│ │ │ │ ├── SplashActivity.java
│ │ │ │ ├── Target.java
│ │ │ │ ├── TargetListAdapter.java
│ │ │ │ ├── TokenActivity.java
│ │ │ │ ├── fragment/
│ │ │ │ │ ├── FragmentActivity1.java
│ │ │ │ │ ├── FragmentActivity2.java
│ │ │ │ │ └── FragmentActivity3.java
│ │ │ │ └── ignoreBatteryOpt.java
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ ├── border_input_box.xml
│ │ │ │ ├── button_background.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ └── token_background.xml
│ │ │ ├── drawable-v24/
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── layout/
│ │ │ │ ├── activity_bluetooth.xml
│ │ │ │ ├── activity_bottom_bar.xml
│ │ │ │ ├── activity_login.xml
│ │ │ │ ├── activity_payload.xml
│ │ │ │ ├── activity_register.xml
│ │ │ │ ├── activity_splash.xml
│ │ │ │ ├── activity_token.xml
│ │ │ │ ├── activity_token_pre.xml
│ │ │ │ ├── activity_user.xml
│ │ │ │ ├── custom_notification.xml
│ │ │ │ └── listview_targets_row.xml
│ │ │ ├── mipmap-anydpi-v26/
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── values/
│ │ │ ├── colors.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── ABC/
│ │ └── pioneer/
│ │ └── app/
│ │ └── ExampleUnitTest.java
│ ├── build.gradle
│ ├── gradle/
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── local.properties
│ ├── pioneer/
│ │ ├── .gitignore
│ │ ├── BuildConfig.java
│ │ ├── app/
│ │ │ └── BuildConfig.java
│ │ ├── build.gradle
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ ├── androidTest/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── ABC/
│ │ │ └── pioneer/
│ │ │ └── sensor/
│ │ │ └── ExampleInstrumentedTest.java
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── ABC/
│ │ │ └── pioneer/
│ │ │ └── sensor/
│ │ │ ├── DefaultSensorDelegate.java
│ │ │ ├── Device.java
│ │ │ ├── PayloadSupplier.java
│ │ │ ├── Sensor.java
│ │ │ ├── SensorArray.java
│ │ │ ├── SensorDelegate.java
│ │ │ ├── analysis/
│ │ │ │ └── Sample.java
│ │ │ ├── ble/
│ │ │ │ ├── BLEDatabaseDelegate.java
│ │ │ │ ├── BLEDevice.java
│ │ │ │ ├── BLESensor.java
│ │ │ │ ├── BluetoothStateManager.java
│ │ │ │ ├── BluetoothStateManagerDelegate.java
│ │ │ │ ├── Configurations.java
│ │ │ │ ├── Database.java
│ │ │ │ ├── DeviceAttribute.java
│ │ │ │ ├── DeviceDelegate.java
│ │ │ │ ├── DeviceOperatingSystem.java
│ │ │ │ ├── DeviceState.java
│ │ │ │ ├── DeviceUpdatedComparator.java
│ │ │ │ ├── Receiver.java
│ │ │ │ ├── SpecificBLESensor.java
│ │ │ │ ├── SpecificBluetoothStateManager.java
│ │ │ │ ├── SpecificDatabase.java
│ │ │ │ ├── SpecificReceiver.java
│ │ │ │ ├── SpecificTransmitter.java
│ │ │ │ ├── Timer.java
│ │ │ │ ├── TimerDelegate.java
│ │ │ │ ├── Transmitter.java
│ │ │ │ ├── TxPower.java
│ │ │ │ └── filter/
│ │ │ │ ├── BLEAppleManuSeg.java
│ │ │ │ ├── BLEDeviceFilter.java
│ │ │ │ ├── BLEManuData.java
│ │ │ │ ├── BLEParser.java
│ │ │ │ ├── BLEScanResponseData.java
│ │ │ │ ├── BLESeg.java
│ │ │ │ ├── BLESegType.java
│ │ │ │ └── BLEServiceData.java
│ │ │ ├── client/
│ │ │ │ └── controller/
│ │ │ │ └── PioneerClient.java
│ │ │ ├── data/
│ │ │ │ ├── BatteryLog.java
│ │ │ │ ├── CalibrationLog.java
│ │ │ │ ├── ConcretePayloadDataFormatter.java
│ │ │ │ ├── ContactLog.java
│ │ │ │ ├── DetectionLog.java
│ │ │ │ ├── EventTimeIntervalLog.java
│ │ │ │ ├── PayloadDataFormatter.java
│ │ │ │ ├── StatisticsLog.java
│ │ │ │ └── TextFile.java
│ │ │ ├── datatype/
│ │ │ │ ├── Base64.java
│ │ │ │ ├── BluetoothState.java
│ │ │ │ ├── Calibration.java
│ │ │ │ ├── CalibrationMeasurementUnit.java
│ │ │ │ ├── Callback.java
│ │ │ │ ├── Data.java
│ │ │ │ ├── Encounter.java
│ │ │ │ ├── Float16.java
│ │ │ │ ├── ImmediateSendData.java
│ │ │ │ ├── InertiaLocationReference.java
│ │ │ │ ├── Int16.java
│ │ │ │ ├── Int32.java
│ │ │ │ ├── Int64.java
│ │ │ │ ├── Int8.java
│ │ │ │ ├── LegacyPayload.java
│ │ │ │ ├── Location.java
│ │ │ │ ├── LocationReference.java
│ │ │ │ ├── PayloadData.java
│ │ │ │ ├── PayloadSharingData.java
│ │ │ │ ├── PayloadTimestamp.java
│ │ │ │ ├── PlacenameLocationReference.java
│ │ │ │ ├── Proximity.java
│ │ │ │ ├── ProximityMeasurementUnit.java
│ │ │ │ ├── PseudoDeviceAddress.java
│ │ │ │ ├── RSSI.java
│ │ │ │ ├── RandomSource.java
│ │ │ │ ├── SensorState.java
│ │ │ │ ├── SensorType.java
│ │ │ │ ├── SignalCharacteristicData.java
│ │ │ │ ├── SignalCharacteristicDataType.java
│ │ │ │ ├── TargetIdentifier.java
│ │ │ │ ├── TimeInterval.java
│ │ │ │ ├── Triple.java
│ │ │ │ ├── Tuple.java
│ │ │ │ ├── UInt16.java
│ │ │ │ ├── UInt32.java
│ │ │ │ ├── UInt64.java
│ │ │ │ └── UInt8.java
│ │ │ ├── motion/
│ │ │ │ ├── ConcreteInertiaSensor.java
│ │ │ │ └── InertiaSensor.java
│ │ │ ├── payload/
│ │ │ │ ├── Crypto/
│ │ │ │ │ ├── BasicFunc.java
│ │ │ │ │ ├── CipherParameters.java
│ │ │ │ │ ├── ContactIdentifier.java
│ │ │ │ │ ├── ContactKey.java
│ │ │ │ │ ├── ContactKeySeed.java
│ │ │ │ │ ├── Digest.java
│ │ │ │ │ ├── DigestRandomGenerator.java
│ │ │ │ │ ├── ExtendedDigest.java
│ │ │ │ │ ├── GeneralDigest.java
│ │ │ │ │ ├── GenerateKey.java
│ │ │ │ │ ├── HMACSHA256.java
│ │ │ │ │ ├── HMac.java
│ │ │ │ │ ├── KeyParameter.java
│ │ │ │ │ ├── Mac.java
│ │ │ │ │ ├── MatchingKey.java
│ │ │ │ │ ├── MatchingKeySeed.java
│ │ │ │ │ ├── Memoable.java
│ │ │ │ │ ├── PioneerHMac.java
│ │ │ │ │ ├── PioneerHash.java
│ │ │ │ │ ├── PioneerPRG.java
│ │ │ │ │ ├── RandomGenerator.java
│ │ │ │ │ ├── SM3Digest.java
│ │ │ │ │ ├── SecretKey.java
│ │ │ │ │ ├── SpecificUsePayloadSupplier.java
│ │ │ │ │ ├── UsePayloadSupplier.java
│ │ │ │ │ └── Util.java
│ │ │ │ ├── DefaultPayloadSupplier.java
│ │ │ │ ├── DigitalSignature.java
│ │ │ │ └── extended/
│ │ │ │ ├── ConcreteExtendedDataSectionV1.java
│ │ │ │ ├── ConcreteExtendedDataV1.java
│ │ │ │ └── ExtendedData.java
│ │ │ └── service/
│ │ │ ├── AlarmReceiver.java
│ │ │ ├── Connection.java
│ │ │ ├── CustomTimer.java
│ │ │ ├── ForegroundService.java
│ │ │ ├── MatchDelegate.java
│ │ │ ├── NotificationService.java
│ │ │ └── PioneerDb.java
│ │ └── test/
│ │ └── java/
│ │ └── com/
│ │ └── ABC/
│ │ └── pioneer/
│ │ └── sensor/
│ │ └── ExampleUnitTest.java
│ └── settings.gradle
├── IOS_app/
│ └── Pioneer_ios/
│ └── Pioneer Framework/
│ ├── Certificate/
│ │ ├── client.p12
│ │ └── server.cer
│ ├── Pioneer/
│ │ ├── ContactLogData/
│ │ │ └── ContactLog.xcdatamodeld/
│ │ │ ├── .xccurrentversion
│ │ │ └── ContactLog.xcdatamodel/
│ │ │ └── contents
│ │ ├── Crypto/
│ │ │ ├── CipherParameters.swift
│ │ │ ├── Crypto.swift
│ │ │ ├── Digest.swift
│ │ │ ├── DigestRandomNumber.swift
│ │ │ ├── ExtendedDigest.swift
│ │ │ ├── GeneralDigest.swift
│ │ │ ├── HMAC.swift
│ │ │ ├── KeyParameter.swift
│ │ │ ├── MAC.swift
│ │ │ ├── Memoable.swift
│ │ │ ├── PioneerHMac.swift
│ │ │ ├── PioneerHash.swift
│ │ │ ├── RandomGenerator.swift
│ │ │ ├── SM3Digest.swift
│ │ │ └── Util.swift
│ │ ├── Info.plist
│ │ ├── Manager/
│ │ │ ├── Connection.swift
│ │ │ ├── GlobalConfiguration.swift
│ │ │ ├── KeyManager.swift
│ │ │ ├── ManagerDelegate.swift
│ │ │ ├── Register.swift
│ │ │ └── UploadManager.swift
│ │ ├── Pioneer.h
│ │ └── Sensor/
│ │ ├── BLE/
│ │ │ ├── BLEDatabase.swift
│ │ │ ├── BLEReceiver.swift
│ │ │ ├── BLESensor.swift
│ │ │ ├── BLETransmitter.swift
│ │ │ └── BLEUtilities.swift
│ │ ├── Data/
│ │ │ ├── BatteryLog.swift
│ │ │ └── SensorLogger.swift
│ │ ├── Device.swift
│ │ ├── Extensions/
│ │ │ ├── DataExtensions.swift
│ │ │ └── DateExtensions.swift
│ │ ├── Location/
│ │ │ └── LocationSensor.swift
│ │ ├── Payload/
│ │ │ ├── PayloadDataMatcher.swift
│ │ │ ├── PayloadDataSupplier.swift
│ │ │ └── SpecificImplement/
│ │ │ ├── SpecificPayloadDataMatcher.swift
│ │ │ └── SpecificPayloadDataSupplier.swift
│ │ ├── Sensor.swift
│ │ ├── SensorArray.swift
│ │ └── SensorDelegate.swift
│ ├── Pioneer.xcodeproj/
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ ├── xcshareddata/
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ └── xcuserdata/
│ │ │ └── Beh.xcuserdatad/
│ │ │ └── UserInterfaceState.xcuserstate
│ │ └── xcuserdata/
│ │ └── Beh.xcuserdatad/
│ │ └── xcschemes/
│ │ └── xcschememanagement.plist
│ └── PioneerTests/
│ ├── Info.plist
│ └── PioneerTests.swift
├── README.md
├── Server/
│ ├── bin/
│ │ ├── db.properties
│ │ └── lib/
│ │ └── mysql-connector-java-5.1.49-bin.jar
│ └── src/
│ ├── com/
│ │ ├── bean/
│ │ │ ├── User.java
│ │ │ └── UserMessage.java
│ │ ├── controller/
│ │ │ ├── PioneerClient.java
│ │ │ └── PioneerServer.java
│ │ ├── dao/
│ │ │ ├── RandomGenerator.java
│ │ │ ├── TaskThread.java
│ │ │ ├── UserDao.java
│ │ │ └── UserDao_Imp.java
│ │ └── jdbc/
│ │ └── JDBCUtils.java
│ ├── db.properties
│ └── lib/
│ └── mysql-connector-java-5.1.49-bin.jar
├── Server_jar/
│ ├── META-INF/
│ │ └── MANIFEST.MF
│ ├── db.properties
│ ├── mysql-connector-java-5.1.49-bin.jar
│ ├── org.hamcrest.core_1.3.0.v20180420-1519.jar
│ └── org.junit_4.13.0.v20200204-1500.jar
└── Test/
├── Contribution.md
└── README.md
================================================
FILE CONTENTS
================================================
================================================
FILE: Android_app/app/.gitignore
================================================
/build
================================================
FILE: Android_app/app/build.gradle
================================================
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.sdu.pioneer.app"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
packagingOptions {
exclude 'classes.dex'
exclude '**.**'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(path: ':pioneer')
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
================================================
FILE: Android_app/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: Android_app/app/src/androidTest/java/com/ABC/pioneer/app/ExampleInstrumentedTest.java
================================================
package com.ABC.pioneer.app;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.sdu.pioneer.app", appContext.getPackageName());
}
}
================================================
FILE: Android_app/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/AppDelegate.java
================================================
package com.ABC.pioneer.app;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.ABC.pioneer.sensor.Sensor;
import com.ABC.pioneer.sensor.SensorArray;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.payload.Crypto.GenerateKey;
import com.ABC.pioneer.sensor.payload.Crypto.SecretKey;
import com.ABC.pioneer.sensor.service.NotificationService;
import java.util.List;
public class AppDelegate extends Application implements SensorDelegate {
private final static String tag =AppDelegate.class.getName();
private final static String NOTIFICATION_CHANNEL_ID = "PIONEER_NOTIFICATION_CHANNEL_ID";
private final static int NOTIFICATION_ID = NOTIFICATION_CHANNEL_ID.hashCode();
private static AppDelegate appDelegate = null;
public static SecretKey secretKey;
private static AppDelegate instance;
/// 生成唯一一致的设备标识符以测试检测和跟踪
private int identifier() {
final String text = Build.MODEL + ":" + Build.BRAND;
return text.hashCode();
}
// 接近检测传感器
static SensorArray sensor = null;
public static int getNotificationId() {
return NOTIFICATION_ID;
}
public static String getNotificationChannelId(){
return NOTIFICATION_CHANNEL_ID;
}
@Override
public PackageManager getPackageManager() {
return super.getPackageManager();
}
@Override
public void onCreate() {
super.onCreate();
final SharedPreferences sp_SecretKey = getSharedPreferences("SecretKey",MODE_PRIVATE);
appDelegate = this;
// 初始化前台服务以使应用程序在后台运行
this.createNotificationChannel();
NotificationService.shared(this).startForegroundService(this.getForegroundNotification(), NOTIFICATION_ID);
// 初始化传感器序列,用于给定的有效负载数据
if(sp_SecretKey.getString("SecretKey","").equals("")){
SharedPreferences.Editor edit_key = sp_SecretKey.edit();
secretKey = GenerateKey.secretKey();
edit_key.putString("SecretKey",secretKey.base64EncodedString());
edit_key.apply();
}
else {
final Data secretkeyData = new Data(sp_SecretKey.getString("SecretKey",""));
secretKey = new SecretKey(secretkeyData.value);
System.out.println(secretKey);
}
final SpecificUsePayloadSupplier payloadDataSupplier = new SpecificUsePayloadSupplier(secretKey);
sensor = new SensorArray(getApplicationContext(), payloadDataSupplier);
// 将appDelegate添加为侦听器,以记录和启动传感器的检测事件
sensor.add(this);
// 效率功能记录
// 测试
//PayloadData payloadData = sensor.payloadData();
// if (BuildConfig.DEBUG) {
// sensor.add(new ContactLog(this, "contacts.csv"));
// sensor.add(new StatisticsLog(this, "statistics.csv",payloadData));
// sensor.add(new DetectionLog(this,"detection.csv", payloadData));
// new BatteryLog(this, "battery.csv");
// if (Configurations.payloadDataUpdateTimeInterval != TimeInterval.never) {
// sensor.add(new EventTimeIntervalLog(this, "statistics_didRead.csv", payloadData, EventTimeIntervalLog.EventType.read));
// }
// }
// 传感器将通过UI开关(默认为ON)和蓝牙状态启动和停止
}
public static AppDelegate getInstance(){
return instance;
}
@Override
public void onTerminate() {
sensor.stop();
super.onTerminate();
}
// 获取应用程序委托
public static AppDelegate getAppDelegate() {
return appDelegate;
}
// 获取传感器
public Sensor sensor() {
return sensor;
}
private Notification getForegroundNotification() {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(this.getString(R.string.notification_content_title))
.setContentText(this.getString(R.string.notification_content_text))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
final Notification notification = builder.build();
return notification;
}
// SensorDelegate用于记录接近检测到的事件
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
final int importance = NotificationManager.IMPORTANCE_DEFAULT;
final NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID,
this.getString(R.string.notification_channel_name), importance);
channel.setDescription(this.getString(R.string.notification_channel_description));
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannel(channel);
}
}
@Override
public void sensor(SensorType sensor, TargetIdentifier didDetect) {
}
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, ImmediateSendData didReceive, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) {
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/LoginActivity.java
================================================
package com.ABC.pioneer.app;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
//登陆界面
public class LoginActivity extends AppCompatActivity {
// 本地变量
private SharedPreferences sp;
private SharedPreferences.Editor edit;
private Button loginbtn;
private Button regisbtn_jump;
private EditText et_phone;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 检查已有登录状态
checkAutoLogin();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 初始化视图
initViews();
// 设置登录按钮事件
setupLoginButton();
// 设置注册跳转按钮事件
setupRegisterButton();
}
/**
* 检查是否已登录,如果是则自动跳转
*/
private void checkAutoLogin() {
sp = getSharedPreferences("PhoneNumber", MODE_PRIVATE);
String savedPhone = sp.getString("PhoneNumber", "");
if (!savedPhone.isEmpty()) {
jumpToMainActivity(getApplication());
finish();
}
}
/**
* 初始化所有视图组件
*/
private void initViews() {
loginbtn = (Button) findViewById(R.id.btn_login);
regisbtn_jump = (Button) findViewById(R.id.btn_register_jump);
et_phone = (EditText) findViewById(R.id.et_phone);
}
/**
* 设置登录按钮点击事件
*/
private void setupLoginButton() {
loginbtn.setOnClickListener(v -> {
String phone = et_phone.getText().toString().trim();
if (!isValidPhone(phone)) {
showToast("请输入正确的手机号");
return;
}
handleLoginLogic(phone);
});
}
/**
* 设置注册跳转按钮点击事件
*/
private void setupRegisterButton() {
regisbtn_jump.setOnClickListener(v -> {
Intent it = new Intent(getApplicationContext(), RegisterActivity.class);
startActivity(it);
});
}
/**
* 验证手机号格式
*/
private boolean isValidPhone(String phone) {
return phone != null && phone.length() == 11;
}
/**
* 处理登录逻辑
*/
private void handleLoginLogic(String phone) {
// 测试账号直接登录
if ("12345678901".equals(phone)) {
jumpToMainActivity(LoginActivity.this);
finish();
return;
}
String savedPhone = sp.getString("PhoneNumber", "");
if (savedPhone.isEmpty()) {
showToast("此电话号码未在本机注册过,请先注册");
} else if (!savedPhone.equals(phone)) {
showToast("此电话号码不是本机注册号码");
} else {
jumpToMainActivity(getApplicationContext());
finish();
}
}
/**
* 跳转到主界面
*/
private void jumpToMainActivity(Context context) {
Intent intent = new Intent(context, MainActivity.class);
startActivity(intent);
}
/**
* 显示Toast提示
*/
private void showToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/MainActivity.java
================================================
package com.ABC.pioneer.app;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.ABC.pioneer.app.fragment.FragmentActivity1;
import com.ABC.pioneer.app.fragment.FragmentActivity2;
import com.ABC.pioneer.app.fragment.FragmentActivity3;
import com.ABC.pioneer.sensor.Sensor;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.service.AlarmReceiver;
import com.ABC.pioneer.sensor.service.CustomTimer;
import com.ABC.pioneer.sensor.service.MatchDelegate;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
public class MainActivity extends AppCompatActivity implements View.OnClickListener,MatchDelegate {
private final static int permissionRequestCode = 1249951875;
private long didDetect = 0, didRead = 0, didMeasure = 0, didShare = 0, didReceive = 0;
private final static int WARNING_NOTIFICATION_ID = "WARNING".hashCode();
private final Map targetIdentifiers = new ConcurrentHashMap<>();
private final Map payloads = new ConcurrentHashMap<>();
private final List targets = new ArrayList<>();
private TargetListAdapter targetListAdapter = null;
private AlertDialog dialog;
private RelativeLayout bottom_bar_bluetooth_btn;
private RelativeLayout bottom_bar_token_btn;
private RelativeLayout bottom_bar_user_btn;
private TextView bottom_bar_text_bluetooth;
private TextView bottom_bar_text_token;
private TextView bottom_bar_text_user;
private ImageView bottom_bar_image_bluetooth;
private ImageView bottom_bar_image_token;
private ImageView bottom_bar_image_user;
private LinearLayout main_bottom_bar;
private LinearLayout main_body;
private int TAG = 0;
private FragmentActivity1 fragmentActivity1;
private FragmentActivity2 fragmentActivity2;
private FragmentActivity3 fragmentActivity3;
public final static Sensor sensor = AppDelegate.getAppDelegate().sensor();
private static MainActivity instance;
public static MainActivity getInstance(){
// 因为我们程序运行后,Application是首先初始化的,如果在这里不用判断instance是否为空
return instance;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instance = MainActivity.this;
// 开启接收器
final AlarmReceiver alarmReceiver = new AlarmReceiver(getApplicationContext(),this);
// 开启定时服务
Intent i = new Intent(this, CustomTimer.class);
startService(i);
setContentView(R.layout.activity_bottom_bar);
initView();
fragmentActivity1 = new FragmentActivity1();
fragmentActivity2 = new FragmentActivity2();
fragmentActivity3 = new FragmentActivity3();
setMain();
// 确保应用具有所有必需的权限
requestPermissions();
ignoreBatteryOpt.ignoreBatteryOptimization(this);
}
@Override
public void matchFound()
{
if(FragmentActivity1.foreground)
{
runOnUiThread(new Runnable() {
@Override
public void run() {
// 如果前台则弹出警告框,警告用户可能被感染
AlertDialog.Builder alertdialogbuilder = new AlertDialog.Builder(MainActivity.this);
alertdialogbuilder.setMessage("传感器检测到您与感染者有接触过,请及时到医院进行核酸检测");
alertdialogbuilder.setPositiveButton("确定", null);
alertdialogbuilder.setCancelable(true);
alertdialogbuilder.setIcon(R.drawable.ic_warning);
alertdialogbuilder.setTitle("接触警告");
final AlertDialog alertdialog1 = alertdialogbuilder.create();
alertdialog1.show();
try {
Field mAlert = AlertDialog.class.getDeclaredField("mAlert");
mAlert.setAccessible(true);
Object mAlertController = mAlert.get(alertdialog1);
Field mMessage = mAlertController.getClass().getDeclaredField("mMessageView");
mMessage.setAccessible(true);
TextView mMessageView = (TextView) mMessage.get(mAlertController);
mMessageView.setTextColor(Color.RED);
Field mTitleView = mAlertController.getClass().getDeclaredField("mTitleView");
mTitleView.setAccessible(true);
TextView title = (TextView) mTitleView.get(mAlertController);
title.setTextColor(Color.RED);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
});
}
else
{
// 如果在后台运行,则弹出一个notification告知用户可能被感染
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(WARNING_NOTIFICATION_ID,getWarningNotification());
}
}
/// 请求传感器操作的应用程序权限。
private void requestPermissions() {
// Check and request permissions
final List requiredPermissions = new ArrayList<>();
requiredPermissions.add(Manifest.permission.BLUETOOTH);
requiredPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
requiredPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
requiredPermissions.add(Manifest.permission.FOREGROUND_SERVICE);
}
requiredPermissions.add(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
requiredPermissions.add(Manifest.permission.WAKE_LOCK);
final String[] requiredPermissionsArray = requiredPermissions.toArray(new String[0]);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(requiredPermissionsArray, permissionRequestCode);
} else {
ActivityCompat.requestPermissions(this, requiredPermissionsArray, permissionRequestCode);
}
}
/// 处理权限结果
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == permissionRequestCode) {
boolean permissionsGranted = true;
for (int i = 0; i < permissions.length; i++) {
final String permission = permissions[i];
if (grantResults[i] != PERMISSION_GRANTED) {
permissionsGranted = false;
} else {
}
}
}
}
// 电量优化
private void showActivity(@NonNull String packageName) {
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
startActivity(intent);
}
private void showActivity(@NonNull String packageName, @NonNull String activityDir) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName, activityDir));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
private Notification getWarningNotification() {
/*final Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);*/
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, AppDelegate.getNotificationChannelId())
.setSmallIcon(R.drawable.ic_notification)
.setColor(Color.RED)
.setContentTitle(this.getString(R.string.warning_notification_content_title))
.setContentText(this.getString(R.string.warning_notification_content_text))
.setWhen(System.currentTimeMillis())
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
final Notification notification = builder.build();
return notification;
}
private void setSelectStatus(int index) {
switch (index){
case 0:
//图片点击选择变换图片,颜色的改变,其他变为原来的颜色,并保持原有的图片
bottom_bar_image_bluetooth.setImageResource(R.drawable.ic_discover_choose);
bottom_bar_text_bluetooth.setTextColor(Color.parseColor("#0097F7"));
//其他的文本颜色不变
bottom_bar_text_token.setTextColor(Color.parseColor("#666666"));
bottom_bar_text_user.setTextColor(Color.parseColor("#666666"));
//图片也不变
bottom_bar_image_token.setImageResource(R.drawable.ic_token_icon);
bottom_bar_image_user.setImageResource(R.drawable.ic_user);
break;
case 1:
bottom_bar_image_token.setImageResource(R.drawable.ic_token_choose);
bottom_bar_text_token.setTextColor(Color.parseColor("#0097F7"));
bottom_bar_text_bluetooth.setTextColor(Color.parseColor("#666666"));
bottom_bar_text_user.setTextColor(Color.parseColor("#666666"));
bottom_bar_image_bluetooth.setImageResource(R.drawable.ic_discover_icon);
bottom_bar_image_user.setImageResource(R.drawable.ic_user);
break;
case 2:
bottom_bar_image_user.setImageResource(R.drawable.ic_user_choose);
bottom_bar_text_user.setTextColor(Color.parseColor("#0097F7"));
bottom_bar_text_token.setTextColor(Color.parseColor("#666666"));
bottom_bar_text_bluetooth.setTextColor(Color.parseColor("#666666"));
bottom_bar_image_bluetooth.setImageResource(R.drawable.ic_discover_icon);
bottom_bar_image_token.setImageResource(R.drawable.ic_token_icon);
break;
}
}
private void initView(){
//底部导航栏
bottom_bar_bluetooth_btn = findViewById(R.id.bottom_bar_bluetooth);
bottom_bar_token_btn = findViewById(R.id.bottom_bar_token);
bottom_bar_user_btn = findViewById(R.id.bottom_bar_user);
bottom_bar_text_bluetooth = findViewById(R.id.bottom_bar_text_bluetooth);
bottom_bar_text_token = findViewById(R.id.bottom_bar_text_token);
bottom_bar_text_user = findViewById(R.id.bottom_bar_text_user);
bottom_bar_image_bluetooth = findViewById(R.id.bottom_bar_image_bluetooth);
bottom_bar_image_token = findViewById(R.id.bottom_bar_image_token);
bottom_bar_image_user = findViewById(R.id.bottom_bar_image_user);
main_bottom_bar = findViewById(R.id.main_body_bar);
main_body = findViewById(R.id.main_body);
//设置点击事件
bottom_bar_bluetooth_btn.setOnClickListener(this);
bottom_bar_token_btn.setOnClickListener(this);
bottom_bar_user_btn.setOnClickListener(this);
}
private void setMain() {
//打开初始界面
TAG = 1;
this.getSupportFragmentManager().beginTransaction().add(R.id.fl_container,fragmentActivity1).commit();
}
@SuppressLint("NonConstantResourceId")
@Override
public void onClick(View v){
if(TAG == 0){
TAG = 1;
switch(v.getId()){
case R.id.bottom_bar_bluetooth:
getSupportFragmentManager().beginTransaction().add(R.id.fl_container,fragmentActivity1).commitAllowingStateLoss();
setSelectStatus(0);
break;
case R.id.bottom_bar_token:
getSupportFragmentManager().beginTransaction().add(R.id.fl_container,fragmentActivity2).commitAllowingStateLoss();
setSelectStatus(1);
break;
case R.id.bottom_bar_user:
getSupportFragmentManager().beginTransaction().add(R.id.fl_container,fragmentActivity3).commitAllowingStateLoss();
setSelectStatus(2);
break;
}
}
else{
switch(v.getId()){
case R.id.bottom_bar_bluetooth:
getSupportFragmentManager().beginTransaction().replace(R.id.fl_container,fragmentActivity1).commitAllowingStateLoss();
setSelectStatus(0);
break;
case R.id.bottom_bar_token:
getSupportFragmentManager().beginTransaction().replace(R.id.fl_container,fragmentActivity2).commitAllowingStateLoss();
setSelectStatus(1);
break;
case R.id.bottom_bar_user:
getSupportFragmentManager().beginTransaction().replace(R.id.fl_container,fragmentActivity3).commitAllowingStateLoss();
setSelectStatus(2);
break;
}
}
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/RegisterActivity.java
================================================
package com.ABC.pioneer.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ABC.pioneer.sensor.SensorArray;
import com.ABC.pioneer.sensor.payload.Crypto.GenerateKey;
import com.ABC.pioneer.sensor.payload.Crypto.SecretKey;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.service.Connection;
import java.io.IOException;
public class RegisterActivity extends AppCompatActivity {
private Button registerButton;
private EditText phoneEditText;
private String serverResponse = "";
private final Context appContext = AppDelegate.getInstance();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
initializeViews();
setupRegisterButton();
}
private void initializeViews() {
registerButton = findViewById(R.id.btn_register);
phoneEditText = findViewById(R.id.et_phone_1);
}
private void setupRegisterButton() {
registerButton.setOnClickListener(v -> handleRegistration());
}
private void handleRegistration() {
String phoneNumber = phoneEditText.getText().toString().trim();
if (!isValidPhoneNumber(phoneNumber)) {
showToast("请输入正确的手机号");
return;
}
performRegistration(phoneNumber);
}
private boolean isValidPhoneNumber(String phoneNumber) {
return phoneNumber.length() == 11;
}
private void performRegistration(String phoneNumber) {
SharedPreferences phonePrefs = getSharedPreferences("PhoneNumber", MODE_PRIVATE);
SharedPreferences secretKeyPrefs = getSharedPreferences("SecretKey", MODE_PRIVATE);
SecretKey currentSecretKey = AppDelegate.secretKey;
new Thread(() -> {
try {
Connection connection = new Connection(RegisterActivity.this);
connection.connectToServer();
serverResponse = connection.register(phoneNumber, currentSecretKey.base64EncodedString());
} catch (IOException e) {
// 处理连接异常
}
}).start().joinWithUiThread(this::processRegistrationResult);
}
private void processRegistrationResult() {
switch (serverResponse) {
case "0":
handleSuccessfulRegistration();
break;
case "1":
showToast("该手机号已被注册");
break;
case "2":
handleSecretKeyConflict();
break;
}
}
private void handleSuccessfulRegistration() {
SharedPreferences.Editor editor = getSharedPreferences("PhoneNumber", MODE_PRIVATE).edit();
editor.putString("PhoneNumber", phoneEditText.getText().toString().trim());
editor.apply();
showToast("注册成功");
}
private void handleSecretKeyConflict() {
showToast("SecretKey重复,已重新生成,请再次点击注册");
SharedPreferences.Editor editor = getSharedPreferences("SecretKey", MODE_PRIVATE).edit();
SecretKey newSecretKey = GenerateKey.secretKey();
editor.putString("SecretKey", newSecretKey.base64EncodedString());
editor.apply();
SpecificUsePayloadSupplier payloadSupplier = new SpecificUsePayloadSupplier(newSecretKey);
AppDelegate.sensor = new SensorArray(getApplicationContext(), payloadSupplier);
AppDelegate.sensor.add(AppDelegate.getAppDelegate());
}
private void showToast(String message) {
runOnUiThread(() ->
Toast.makeText(RegisterActivity.this, message, Toast.LENGTH_LONG).show()
);
}
// 辅助方法:在UI线程执行回调
private interface UiThreadCallback {
void execute();
}
private static void joinWithUiThread(Thread thread, Activity activity, UiThreadCallback callback) {
try {
thread.join();
activity.runOnUiThread(callback::execute);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/SplashActivity.java
================================================
package com.ABC.pioneer.app;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
private final int SPLASH_DISPLAY_LENGHT = 2000; // 两秒后进入系统
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
Handler x = new Handler();//定义一个handle对象
x.postDelayed(new splashhandler(), 3000);//设置3秒钟延迟执行splashhandler线程。其实你这里可以再新建一个线程去执行初始化工作,如判断SD,网络状态等
}
class splashhandler implements Runnable{
public void run() {
startActivity(new Intent(getApplicationContext(),LoginActivity.class));// 这个线程的作用3秒后就是进入到你的登录界面
SplashActivity.this.finish();// 把当前的LaunchActivity结束掉
}
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/Target.java
================================================
package com.ABC.pioneer.app;
import com.ABC.pioneer.sensor.analysis.Sample;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.util.Date;
public class Target {
private TargetIdentifier targetIdentifier = null;
private PayloadData payloadData = null;
private Date lastUpdatedAt = null;
private Proximity proximity = null;
private ImmediateSendData received = null;
private Date didRead = null, didMeasure = null, didShare = null, didReceive = null;
private Sample didReadTimeInterval = new Sample();
private Sample didMeasureTimeInterval = new Sample();
private Sample didShareTimeInterval = new Sample();
public Target(TargetIdentifier targetIdentifier, PayloadData payloadData) {
this.targetIdentifier = targetIdentifier;
this.payloadData = payloadData;
lastUpdatedAt = new Date();
didRead = lastUpdatedAt;
}
public TargetIdentifier targetIdentifier() {
return targetIdentifier;
}
public void targetIdentifier(TargetIdentifier targetIdentifier) {
//lastUpdatedAt = new Date();
this.targetIdentifier = targetIdentifier;
}
public PayloadData payloadData() {
return payloadData;
}
public Date lastUpdatedAt() {
return lastUpdatedAt;
}
public Proximity proximity() {
return proximity;
}
public void proximity(Proximity proximity) {
final Date date = new Date();
if (didMeasure != null) {
final TimeInterval timeInterval = new TimeInterval(didMeasure, date);
didMeasureTimeInterval.add(timeInterval.value);
}
/*lastUpdatedAt = date;
didMeasure = lastUpdatedAt;*/
didMeasure = date;
this.proximity = proximity;
}
public ImmediateSendData received() {
return received;
}
public void received(ImmediateSendData received) {
lastUpdatedAt = new Date();
didReceive = lastUpdatedAt;
this.received = received;
}
public Date didRead() {
return didRead;
}
public Sample didReadTimeInterval() { return didReadTimeInterval; }
public void didRead(Date date) {
if (didRead != null && date != null) {
final TimeInterval timeInterval = new TimeInterval(didRead, date);
didReadTimeInterval.add(timeInterval.value);
}
didRead = date;
lastUpdatedAt = didRead;
}
public Date didMeasure() {
return didMeasure;
}
public Sample didMeasureTimeInterval() {
return didMeasureTimeInterval;
}
public Date didShare() {
return didShare;
}
public void didShare(Date date) {
if (didShare != null && date != null) {
final TimeInterval timeInterval = new TimeInterval(didShare, date);
didShareTimeInterval.add(timeInterval.value);
}
didShare = date;
lastUpdatedAt = didShare;
}
public Sample didShareTimeInterval() {
return didShareTimeInterval;
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/TargetListAdapter.java
================================================
package com.ABC.pioneer.app;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
//用于在UI上显示目标列表的目标列表适配器
public class TargetListAdapter extends ArrayAdapter {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
public TargetListAdapter(@NonNull Context context, List targets) {
super(context, R.layout.listview_targets_row, targets);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
final Target target = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.listview_targets_row, parent, false);
}
final TextView textLabel = (TextView) convertView.findViewById(R.id.targetTextLabel);
final TextView detailedTextLabel = (TextView) convertView.findViewById(R.id.targetDetailedTextLabel);
SimpleDateFormat Dateformat=new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
Dateformat.setTimeZone(TimeZone.getTimeZone("GMT+08"));
final StringBuilder labelText = new StringBuilder(target.payloadData().shortName());
final String didReceive = " (接收时间 : " + Dateformat.format(SpecificUsePayloadSupplier.parseStartTimeToLong(target.payloadData())) + ")";
textLabel.setText(labelText.toString() + didReceive);
if(SpecificUsePayloadSupplier.checkPayloadtime(target.payloadData())) {
detailedTextLabel.setText("更新时间 : " + dateFormatter.format(target.lastUpdatedAt()));
}
return convertView;
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/TokenActivity.java
================================================
// Android APP
// Token activity
package com.ABC.pioneer.app;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.payload.Crypto.GenerateKey;
import com.ABC.pioneer.sensor.payload.Crypto.MatchingKey;
import com.ABC.pioneer.sensor.service.Connection;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Date;
// token activity
public class TokenActivity extends AppCompatActivity {
private Button tokenbtn;
private EditText tokenet;
DataInputStream in=null;
DataOutputStream out=null;
private String Maching_keys="";
private SharedPreferences sp;
String result = "";
String result1 = "";
Connection connection;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_token);
sp = getSharedPreferences("PhoneNumber",MODE_PRIVATE);
tokenbtn = (Button)findViewById(R.id.btn_token);
tokenet = (EditText)findViewById(R.id.et_token);
MatchingKey[] MachingKeys = SpecificUsePayloadSupplier.matchingKeys;
tokenbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/*根据当前时间和app开启时间的时间差计算距开启已经过多少天,用天数i定位到MachingKeys[i],
这个即为当天的Machingkey,然后再向前找14天的Machingkey*/
final int date = GenerateKey.day(new Date());
String token = tokenet.getText().toString();
if (token.trim().length() != 6) {
Toast.makeText(TokenActivity.this, "请输入正确的Token号码", Toast.LENGTH_LONG).show();
return;
}
Thread thread = new Thread(new Runnable(){
@Override
public void run(){
try{
connection = new Connection(TokenActivity.this);
connection.ConnectToServer();
result = connection.Token(token);
if(result.equals("0")) {
String phone = sp.getString("PhoneNumber","默认值");
for(int i = 13; i >= 0; i--){
Maching_keys += MachingKeys[date - i].base64EncodedString();
}
result1 = connection.Token_TransmitMachingKeys(phone,Maching_keys);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(result.equals("0")) {
Toast.makeText(TokenActivity.this, "验证通过", Toast.LENGTH_LONG).show();
Toast.makeText(TokenActivity.this, "成功上传ID和14天Maching_keys", Toast.LENGTH_LONG).show();
}
else if(result.equals("1")){
Toast.makeText(TokenActivity.this, "Token输入有误,请重新输入!", Toast.LENGTH_LONG).show();
}
}
});
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/fragment/FragmentActivity1.java
================================================
package com.ABC.pioneer.app.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.ABC.pioneer.app.MainActivity;
import com.ABC.pioneer.app.R;
import com.ABC.pioneer.app.Target;
import com.ABC.pioneer.app.TargetListAdapter;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.service.PioneerDb;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
public class FragmentActivity1 extends Fragment implements AdapterView.OnItemClickListener, SensorDelegate {
public static boolean foreground = false;
private long didDetect = 0, didRead = 0, didMeasure = 0, didShare = 0, didReceive = 0;
private final Map targetIdentifiers = new ConcurrentHashMap<>();
private final Map payloads = new ConcurrentHashMap<>();
private final List targets = new ArrayList<>();
private TargetListAdapter targetListAdapter = null;
private AlertDialog dialog;
private View view;
private final Context context = MainActivity.getInstance();
private TextView Read;
private TextView Measure;
private TextView Detect;
private TextView Share;
private TextView count;
private ListView targetsListView;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle saveInstanceState){
android.view.View view = inflater.inflate(R.layout.activity_bluetooth,container,false);
this.view = view;
initView(view);
return view;
}
private void initView(View view){
Read = (TextView)view.findViewById(R.id.didReadCount);
Measure = (TextView)view.findViewById(R.id.didMeasureCount);
Detect = (TextView)view.findViewById(R.id.didDetectCount);
Share = (TextView)view.findViewById(R.id.didShareCount);
count = (TextView)view.findViewById(R.id.detection);
targetsListView = (ListView)view.findViewById(R.id.targets);
}
@Override
public void onActivityCreated(Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
//测试特定于UI的过程,以从传感器收集数据进行展示
@SuppressLint("UseSwitchCompatOrMaterialCode") final Switch onOffSwitch = Objects.requireNonNull(getActivity()).findViewById(R.id.sensorOnOffSwitch);
MainActivity.sensor.add(this);
onOffSwitch.setChecked(true);
MainActivity.sensor.start();
onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
MainActivity.sensor.start();
} else {
MainActivity.sensor.stop();
}
}
});
//测试特定于UI的过程,以从传感器收集数据进行展示
targetListAdapter = new TargetListAdapter(context, targets);
targetsListView.setAdapter(targetListAdapter);
targetsListView.setOnItemClickListener(this);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle saveInstanceState){
super.onViewCreated(view, saveInstanceState);
}
// 更新目标表
private synchronized void updateTargets() {
// 根据短名称删除重复目标,并在时间戳上最后更新
final Map shortNames = new HashMap<>(payloads.size());
for (Map.Entry entry : payloads.entrySet()) {
final String shortName = entry.getKey().shortName();
final Target target = entry.getValue();
final Target duplicate = shortNames.get(shortName);
if (duplicate == null || duplicate.lastUpdatedAt().getTime() < target.lastUpdatedAt().getTime()) {
shortNames.put(shortName, target);
}
}
// 按字母顺序获取目标列表
final List targetList = new ArrayList<>(shortNames.values());
Collections.sort(targetList, new Comparator() {
@Override
public int compare(Target t0, Target t1) {
return t0.payloadData().shortName().compareTo(t1.payloadData().shortName());
}
});
// 更新用户界面
count.setText("接收数据包 (" + targetListAdapter.getCount() + ")");
targetListAdapter.clear();
targetListAdapter.addAll(targetList);
}
// 更新相应检测结果
private void updateCounts() {
Detect.setText(Long.toString(this.didDetect));
Read.setText(Long.toString(this.didRead));
Measure.setText(Long.toString(this.didMeasure));
Share.setText(Long.toString(this.didShare));
}
@Override
public void onResume() {
super.onResume();
foreground = true;
updateCounts();
updateTargets();
}
@Override
public void onPause() {
foreground = false;
super.onPause();
}
@Override
public void sensor(SensorType sensor, TargetIdentifier didDetect) {
this.didDetect++;
if (foreground) {
final String text = Long.toString(this.didDetect);
Objects.requireNonNull(getActivity()).runOnUiThread(new Runnable() {
@Override
public void run() {
Detect.setText(text);
}
});
}
}
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
this.didRead++;
targetIdentifiers.put(fromTarget, didRead);
Target target = payloads.get(didRead);
if (target != null) {
target.didRead(new Date());
} else {
payloads.put(didRead, new Target(fromTarget, didRead));
}
if (foreground) {
final String text = Long.toString(this.didRead);
Objects.requireNonNull(getActivity()).runOnUiThread(new Runnable() {
@Override
public void run() {
Read.setText(text);
updateTargets();
}
});
}
PioneerDb db = new PioneerDb(context,"payloads",null,1);
db.insertPayloadData(didRead);
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
this.didShare++;
final Date now = new Date();
for (PayloadData didRead : didShare) {
targetIdentifiers.put(fromTarget, didRead);
Target target = payloads.get(didRead);
if (target != null) {
target.didRead(new Date());
} else {
payloads.put(didRead, new Target(fromTarget, didRead));
}
}
if (foreground) {
final String text = Long.toString(this.didShare);
Objects.requireNonNull(getActivity()).runOnUiThread(new Runnable() {
@Override
public void run() {
Share.setText(text);
updateTargets();
}
});
}
}
@Override
public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) {
this.didMeasure++;
final PayloadData didRead = targetIdentifiers.get(fromTarget);
if (didRead != null) {
final Target target = payloads.get(didRead);
if (target != null) {
target.targetIdentifier(fromTarget);
target.proximity(didMeasure);
}
}
if (foreground) {
final String text = Long.toString(this.didMeasure);
Objects.requireNonNull(getActivity()).runOnUiThread(new Runnable() {
@Override
public void run() {
Measure.setText(text);
}
});
}
}
@Override
public void sensor(SensorType sensor, ImmediateSendData didReceive, TargetIdentifier fromTarget) {
this.didReceive++;
final PayloadData didRead = new PayloadData(didReceive.data.value);
if (didRead != null) {
final Target target = payloads.get(didRead);
if (target != null) {
targetIdentifiers.put(fromTarget, didRead);
target.targetIdentifier(fromTarget);
target.received(didReceive);
}
}
if (foreground) {
final String text = Long.toString(this.didReceive);
Objects.requireNonNull(getActivity()).runOnUiThread(new Runnable() {
@Override
public void run() {
updateTargets();
}
});
}
}
@Override
public void onItemClick(AdapterView> adapter, View view, int i, long l) {
final Target target = targetListAdapter.getItem(i);
final PayloadData payloadData = target.payloadData();
SimpleDateFormat dateformat=new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
dateformat.setTimeZone(TimeZone.getTimeZone("GMT+08"));
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
LayoutInflater inflater = (LayoutInflater) Objects.requireNonNull(getContext()).getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View messageview = inflater.inflate(R.layout.activity_payload, null);
TextView ContactIdentifier = (TextView)messageview.findViewById(R.id.ContactIdentifier);
TextView StartTime = (TextView)messageview.findViewById(R.id.StartTime);
TextView EndTime = (TextView)messageview.findViewById(R.id.EndTime);
ContactIdentifier.append(SpecificUsePayloadSupplier.parseContactIdentifierToStr(payloadData));
final long startTime = SpecificUsePayloadSupplier.parseStartTimeToLong(payloadData);
StartTime.append(dateformat.format(startTime));
EndTime.append(dateformat.format(startTime+360000));
builder.setView(messageview);
builder.create();
dialog = builder.show();
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/fragment/FragmentActivity2.java
================================================
package com.ABC.pioneer.app.fragment;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.ABC.pioneer.app.MainActivity;
import com.ABC.pioneer.app.R;
import com.ABC.pioneer.app.TokenActivity;
public class FragmentActivity2 extends Fragment {
LinearLayout token_pre;
private final Context context = MainActivity.getInstance();
private AlertDialog dialog;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle saveInstanceState){
android.view.View view = inflater.inflate(R.layout.activity_token_pre,container,false);
initView(view);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
token_pre.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent it=new Intent(getContext(), TokenActivity.class);//启动TokenActivity
startActivity(it);
}
});
}
private void initView(View view){
token_pre = (LinearLayout) view.findViewById(R.id.token_pre);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle saveInstanceState) {
super.onViewCreated(view, saveInstanceState);
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/fragment/FragmentActivity3.java
================================================
package com.ABC.pioneer.app.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.ABC.pioneer.app.R;
import com.ABC.pioneer.sensor.SensorArray;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import java.util.Date;
public class FragmentActivity3 extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle saveInstanceState){
android.view.View view = inflater.inflate(R.layout.activity_user,container,false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
((TextView) getActivity().findViewById(R.id.device)).setText("设备名:" + SensorArray.deviceDescription);
((TextView) getActivity().findViewById(R.id.payload)).setText("本机数据包:"+SpecificUsePayloadSupplier.updatePayload(new Date()).shortName());
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle saveInstanceState){
super.onViewCreated(view, saveInstanceState);
}
}
================================================
FILE: Android_app/app/src/main/java/com/ABC/pioneer/app/ignoreBatteryOpt.java
================================================
package com.ABC.pioneer.app;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.NonNull;
import static android.content.Context.POWER_SERVICE;
public class ignoreBatteryOpt {
static public void ignoreBatteryOptimization(Activity activity) {
PowerManager powerManager = (PowerManager)activity.getSystemService(POWER_SERVICE);
boolean hasIgnored = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
// 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。
if(!hasIgnored) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:"+activity.getPackageName()));
activity.startActivity(intent);
}
}
}
}
================================================
FILE: Android_app/app/src/main/res/drawable/border_input_box.xml
================================================
================================================
FILE: Android_app/app/src/main/res/drawable/button_background.xml
================================================
-
================================================
FILE: Android_app/app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: Android_app/app/src/main/res/drawable/token_background.xml
================================================
-
================================================
FILE: Android_app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_bluetooth.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_bottom_bar.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_login.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_payload.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_register.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_splash.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_token.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_token_pre.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/activity_user.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/custom_notification.xml
================================================
================================================
FILE: Android_app/app/src/main/res/layout/listview_targets_row.xml
================================================
================================================
FILE: Android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: Android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: Android_app/app/src/main/res/values/colors.xml
================================================
#6200EE
#3700B3
#03DAC5
#8E8E93
#F2F2F7
#FFFFFF
#34C759
#FF9500
#FF3B30
#007AFF
#FFFFFF
================================================
FILE: Android_app/app/src/main/res/values/ic_launcher_background.xml
================================================
#ffffff
================================================
FILE: Android_app/app/src/main/res/values/strings.xml
================================================
Pioneer
Pioneer Status
Notifications for current status of Pioneer
接触追踪
传感器运行中
接触警告
传感器检测到您与感染者有接触过,请及时到医院进行核酸检测
================================================
FILE: Android_app/app/src/main/res/values/styles.xml
================================================
================================================
FILE: Android_app/app/src/test/java/com/ABC/pioneer/app/ExampleUnitTest.java
================================================
package com.ABC.pioneer.app;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see Testing documentation
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: Android_app/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: Android_app/gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Apr 23 18:48:51 CST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
================================================
FILE: Android_app/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
================================================
FILE: Android_app/gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: Android_app/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: Android_app/local.properties
================================================
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Sat May 08 22:37:11 CST 2021
sdk.dir=C\:\\Users\\12268\\AppData\\Local\\Android\\Sdk
================================================
FILE: Android_app/pioneer/.gitignore
================================================
/build
================================================
FILE: Android_app/pioneer/BuildConfig.java
================================================
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.vmware.herald.app;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.vmware.herald.app";
public static final String BUILD_TYPE = "debug";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.2.0";
}
================================================
FILE: Android_app/pioneer/app/BuildConfig.java
================================================
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.ABC.pioneer.app;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.vmware.herald.app";
public static final String BUILD_TYPE = "debug";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.2.0";
}
================================================
FILE: Android_app/pioneer/build.gradle
================================================
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'org.jetbrains:annotations:15.0'
testImplementation 'junit:junit:4.12'
testImplementation "org.json:json:20201115"
}
================================================
FILE: Android_app/pioneer/consumer-rules.pro
================================================
================================================
FILE: Android_app/pioneer/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: Android_app/pioneer/src/androidTest/java/com/ABC/pioneer/sensor/ExampleInstrumentedTest.java
================================================
package com.ABC.pioneer.sensor;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.sdu.pioneer.sensor.test", appContext.getPackageName());
}
}
================================================
FILE: Android_app/pioneer/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/DefaultSensorDelegate.java
================================================
package com.ABC.pioneer.sensor;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.List;
/// SensorDelegate的默认实现,用于使所有接口方法都可选。
public abstract class DefaultSensorDelegate implements SensorDelegate {
@Override
public void sensor(SensorType sensor, TargetIdentifier didDetect) {
}
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, ImmediateSendData didReceive, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
}
@Override
public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) {
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/Device.java
================================================
package com.ABC.pioneer.sensor;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.Date;
public class Device {
///设备注册时间戳
public final Date createdAt;
///上次任何更改,例如attribute更新
public Date lastUpdatedAt = null;
//临时设备标识符,例如 外设标识符UUID
public final TargetIdentifier identifier;
public Device(TargetIdentifier identifier) {
this.createdAt = new Date();
this.lastUpdatedAt = this.createdAt;
this.identifier = identifier;
}
public Device(Device device, TargetIdentifier identifier) {
this.createdAt = device.createdAt;
this.lastUpdatedAt = new Date();
this.identifier = identifier;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/PayloadSupplier.java
================================================
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0
//
package com.ABC.pioneer.sensor;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.LegacyPayload;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadTimestamp;
import java.util.List;
public interface PayloadSupplier {
LegacyPayload legacyPayload(PayloadTimestamp timestamp, Device device);
// 获取给定时间戳的有效负载。 使用它与任何有效负载生成器集成
PayloadData payload(PayloadTimestamp timestamp, Device device);
/// 将原始数据解析为有效载荷
List payload(Data data);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/Sensor.java
================================================
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0
//
package com.ABC.pioneer.sensor;
///用于检测和跟踪各种疾病传播媒介的传感器
public interface Sensor {
/// 添加delegate以响应传感器事件。
void add(SensorDelegate delegate);
/// 开始检测。
void start();
/// 停止检测。
void stop();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/SensorArray.java
================================================
package com.ABC.pioneer.sensor;
import android.content.Context;
import com.ABC.pioneer.sensor.ble.Configurations;
import com.ABC.pioneer.sensor.ble.SpecificBLESensor;
import com.ABC.pioneer.sensor.data.CalibrationLog;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadTimestamp;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.motion.ConcreteInertiaSensor;
import java.util.ArrayList;
import java.util.List;
// 用来集成多种探测和追踪方法的传感器列表
public class SensorArray implements Sensor {
private final Context context;
private final List sensorArray = new ArrayList<>();
private final PayloadData payloadData;
public final static String deviceDescription = android.os.Build.MODEL + " (Android " + android.os.Build.VERSION.SDK_INT + ")";
private final SpecificBLESensor specificBleSensor;
public SensorArray(final Context context, PayloadSupplier payloadSupplier) {
this.context = context;
// 定义 sensor 列表
specificBleSensor = new SpecificBLESensor(context, payloadSupplier);
sensorArray.add(specificBleSensor);
//惯性传感器配置用于自动RSSI距离校准数据捕获
if (Configurations.inertiaSensorEnabled) {
sensorArray.add(new ConcreteInertiaSensor(context));
add(new CalibrationLog(context, "calibration.csv"));
}
payloadData = payloadSupplier.payload(new PayloadTimestamp(), null);
}
///立即发送数据。
public boolean immediateSend(Data data, TargetIdentifier targetIdentifier) {
return specificBleSensor.immediateSend(data,targetIdentifier);
}
///立即发送给所有人(已连接/最近/附近)
public boolean immediateSendAll(Data data) {
return specificBleSensor.immediateSendAll(data);
}
public final PayloadData payloadData() {
return payloadData;
}
@Override
public void add(final SensorDelegate delegate) {
for (Sensor sensor : sensorArray) {
sensor.add(delegate);
}
}
@Override
public void start() {
for (Sensor sensor : sensorArray) {
sensor.start();
}
}
@Override
public void stop() {
for (Sensor sensor : sensorArray) {
sensor.stop();
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/SensorDelegate.java
================================================
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0
//
package com.ABC.pioneer.sensor;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.List;
// 用来接受传感事件的传感器代理
public interface SensorDelegate {
// 用临时身份标识符探测周边设备
void sensor(SensorType sensor, TargetIdentifier didDetect);
// 从目标处读取payload
void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget);
// 从目标处写下立即传送的数据
void sensor(SensorType sensor, ImmediateSendData didReceive, TargetIdentifier fromTarget);
// 从一个目标处读取该目标最近从其它设备接收到的payload
void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget);
// 测量和一个目标的接近度
void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/analysis/Sample.java
================================================
package com.ABC.pioneer.sensor.analysis;
public class Sample {
protected long n = 0;
protected double m1 = 0, m2 = 0, m3 = 0, m4 = 0;
private double min = Double.MAX_VALUE, max = -Double.MAX_VALUE;
public Sample() {
}
public Sample(final double x, final long f) {
n = f;
m1 = x;
min = x;
max = x;
}
public synchronized void add(final double x) {
final long n1 = n;
n++;
final double delta = x - m1;
final double delta_n = delta / n;
final double delta_n2 = delta_n * delta_n;
final double term1 = delta * delta_n * n1;
m1 += delta_n;
m4 += term1 * delta_n2 * (n * n - 3 * n + 3) + 6 * delta_n2 * m2 - 4 * delta_n * m3;
m3 += term1 * delta_n * (n - 2) - 3 * delta_n * m2;
m2 += term1;
if (x < min) {
min = x;
}
if (x > max) {
max = x;
}
}
public void add(final double x, final long f) {
add(new Sample(x, f));
}
/**
* 将另一个样本分布合并到当前分布中
* 用于累积统计量计算,支持在线算法更新统计量
*
* @param distribution 要合并的样本分布
*/
public void add(final Sample distribution) {
// 如果当前样本为空,直接复制另一个样本的所有统计量
if (n == 0) {
copyStatisticsFrom(distribution);
return;
}
// 计算合并后的样本统计量
Sample combined = combineStatistics(distribution);
// 更新当前样本的统计量
updateCurrentStatistics(combined);
}
/**
* 从另一个样本复制所有统计量到当前样本
*/
private void copyStatisticsFrom(Sample source) {
n = source.n;
m1 = source.m1;
m2 = source.m2;
m3 = source.m3;
m4 = source.m4;
min = source.min;
max = source.max;
}
/**
* 计算合并后的统计量
*/
private Sample combineStatistics(Sample other) {
Sample combined = new Sample();
combined.n = n + other.n;
// 计算均值差异及其幂次
final double delta = other.m1 - m1;
final double delta2 = delta * delta;
final double delta3 = delta * delta2;
final double delta4 = delta2 * delta2;
// 计算合并后的各阶中心矩
combined.m1 = calculateCombinedMean(other, combined.n);
combined.m2 = calculateCombinedSecondMoment(other, delta2, combined.n);
combined.m3 = calculateCombinedThirdMoment(other, delta, delta3, combined.n);
combined.m4 = calculateCombinedFourthMoment(other, delta, delta2, delta4, combined.n);
// 更新最小最大值
combined.min = Math.min(min, other.min);
combined.max = Math.max(max, other.max);
return combined;
}
/**
* 计算合并后的均值
*/
private double calculateCombinedMean(Sample other, double combinedN) {
return (n * m1 + other.n * other.m1) / combinedN;
}
/**
* 计算合并后的二阶中心矩
*/
private double calculateCombinedSecondMoment(Sample other, double delta2, double combinedN) {
return m2 + other.m2 + delta2 * n * other.n / combinedN;
}
/**
* 计算合并后的三阶中心矩
*/
private double calculateCombinedThirdMoment(Sample other, double delta, double delta3, double combinedN) {
double term1 = m3 + other.m3;
double term2 = delta3 * n * other.n * (n - other.n) / (combinedN * combinedN);
double term3 = 3.0 * delta * (n * other.m2 - other.n * m2) / combinedN;
return term1 + term2 + term3;
}
/**
* 计算合并后的四阶中心矩
*/
private double calculateCombinedFourthMoment(Sample other, double delta, double delta2, double delta4, double combinedN) {
double term1 = m4 + other.m4;
double term2 = delta4 * n * other.n * (n * n - n * other.n + other.n * other.n)
/ (combinedN * combinedN * combinedN);
double term3 = 6.0 * delta2 * (n * n * other.m2 + other.n * other.n * m2)
/ (combinedN * combinedN);
double term4 = 4.0 * delta * (n * other.m3 - other.n * m3) / combinedN;
return term1 + term2 + term3 + term4;
}
/**
* 用合并后的统计量更新当前样本
*/
private void updateCurrentStatistics(Sample combined) {
n = combined.n;
m1 = combined.m1;
m2 = combined.m2;
m3 = combined.m3;
m4 = combined.m4;
min = combined.min;
max = combined.max;
}
public long count() {
return n;
}
//平均数
public Double mean() {
if (n > 0) {
return m1;
} else {
return null;
}
}
/**
* 计算并返回样本方差
* 方差是衡量数据离散程度的指标
*
* @return 样本方差值,如果样本数量不足则返回null
*/
public Double variance() {
if (n > 1) {
return m2 / (n - 1d);
} else {
return null;
}
}
/**
* 计算并返回样本标准差
* 标准差是方差的平方根,表示数据分布的离散程度
*
* @return 样本标准差,如果样本数量不足则返回null
*/
public Double standardDeviation() {
if (n > 1) {
return StrictMath.sqrt(m2 / (n - 1d));
} else {
return null;
}
}
/**
* 获取样本中的最小值
*
* @return 样本最小值,如果样本为空则返回null
*/
public Double min() {
if (n > 0) {
return min;
} else {
return null;
}
}
/**
* 获取样本中的最大值
*
* @return 样本最大值,如果样本为空则返回null
*/
public Double max() {
if (n > 0) {
return max;
} else {
return null;
}
}
/**
* 估计当前样本分布与另一个样本分布之间的距离
* 值为1表示分布相同,0表示完全不同
*
* @param otherSample 要比较的另一个样本
* @return 分布距离值,如果无法计算则返回null
*/
public Double distance(final Sample sample) {
return bhattacharyyaDistance(this, sample);
}
/**
* 计算两个分布之间的Bhattacharyya距离
* 用于估计两个分布相同的可能性
* 值为1表示两个分布完全相同,0表示完全不同
*
* @param firstSample 第一个样本
* @param secondSample 第二个样本
* @return Bhattacharyya距离值,如果无法计算则返回null
*/
private final static Double bhattacharyyaDistance(final Sample d1, final Sample d2) {
final Double v1 = d1.variance();
final Double v2 = d2.variance();
final Double m1 = d1.mean();
final Double m2 = d2.mean();
if (v1 == null || v2 == null || m1 == null || m2 == null) {
return null;
}
if (v1 == 0 && v2 == 0) {
if (m1 == m2) {
return 1.0;
} else {
return 0.0;
}
}
final Double sd1 = Math.sqrt(v1);
final Double sd2 = Math.sqrt(v2);
if (sd1 == null || sd2 == null) {
return null;
}
final double Dbc = Math.sqrt((2.0 * sd1 * sd2) / (v1 + v2))
* Math.exp(-1.0 / 4.0 * (Math.pow((m1 - m2), 2) / (v1 + v2)));
return Dbc;
}
/**
* 返回样本的字符串表示形式
*
* @return 包含样本统计信息的字符串
*/
@Override
public String toString() {
return String.format(
"[count=%d, mean=%.2f, sd=%.2f, min=%.2f, max=%.2f]",
count(),
mean(),
standardDeviation(),
min(),
max()
);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/BLEDatabaseDelegate.java
================================================
package com.ABC.pioneer.sensor.ble;
/// 代表接收注册表创建/更新/删除事件
public interface BLEDatabaseDelegate {
void bleDatabaseDidCreate(BLEDevice device);
void bleDatabaseDidUpdate(BLEDevice device, DeviceAttribute attribute);
void bleDatabaseDidDelete(BLEDevice device);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/BLEDevice.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.le.ScanRecord;
import com.ABC.pioneer.sensor.Device;
import com.ABC.pioneer.sensor.datatype.Calibration;
import com.ABC.pioneer.sensor.datatype.CalibrationMeasurementUnit;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PseudoDeviceAddress;
import com.ABC.pioneer.sensor.datatype.RSSI;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Queue;
public class BLEDevice extends Device {
/// BLE characteristics特征
private BluetoothGattCharacteristic signalCharacteristic = null;
private BluetoothGattCharacteristic payloadCharacteristic = null;
private BluetoothGattCharacteristic legacyPayloadCharacteristic = null;
protected byte[] signalCharacteristicWriteValue = null;
protected Queue signalCharacteristicWriteQueue = null;
private BluetoothGattCharacteristic modelCharacteristic = null;
private String model = null;
private BluetoothGattCharacteristic deviceNameCharacteristic = null;
private String deviceName = null;
/// 跟踪连接时间戳
private Date lastDiscoveredAt = null;
private Date lastConnectedAt = null;
/// 有效负载数据已与此对等方共享
protected final List payloadSharingData = new ArrayList<>();
/// 跟踪写入时间戳
private Date lastWritePayloadAt = null;
private Date lastWriteRssiAt = null;
private Date lastWritePayloadSharingAt = null;
// 伪设备地址,用于跟踪不断更改地址的Android设备。
private PseudoDeviceAddress pseudoDeviceAddress = null;
// 用于侦听属性更新事件的委托
private final DeviceDelegate delegate;
/// 用于与此设备进行交互的Android蓝牙设备对象。
private BluetoothDevice peripheral = null;
/// 蓝牙设备连接状态。
private DeviceState state = DeviceState.disconnected;
/// 设备操作系统,这对于为每个平台选择不同的交互过程是必需的。
private DeviceOperatingSystem operatingSystem = DeviceOperatingSystem.unknown;
//通过payloadCharacteristic读取从设备获取的有效载荷数据
private PayloadData payloadData = null;
private Date lastPayloadDataUpdate = null;
/// 立即发送数据以发送到下一个设备
private Data immediateSendData = null;
/// 通过readRSSI或didDiscover进行最新的RSSI测量。
private RSSI rssi = null;
/// 在可用的情况下传输功率数据(仅由Android设备提供)
private TxPower txPower = null;
/// 如果设备是receive only设备?
private boolean receiveOnly = false;
/// 相应的ignore设置
private TimeInterval ignoreForDuration = null;
private Date ignoreUntil = null;
private ScanRecord scanRecord = null;
public TimeInterval timeIntervalSinceConnected() {
if (state() != DeviceState.connected) {
return TimeInterval.zero;
}
if (lastConnectedAt == null) {
return TimeInterval.zero;
}
return new TimeInterval((new Date().getTime() - lastConnectedAt.getTime()) / 1000);
}
/// 自上次属性值更新以来的时间间隔,此间隔用于标识可能已过期并且应从数据库中删除的设备。
public TimeInterval timeIntervalSinceLastUpdate() {
if (lastUpdatedAt == null) {
return TimeInterval.never;
}
return new TimeInterval((new Date().getTime() - lastUpdatedAt.getTime()) / 1000);
}
public String description() {
return "BLEDevice[" +
"id=" + identifier +
",os=" + operatingSystem +
",payload=" + payloadData() +
(pseudoDeviceAddress() != null ? ",address=" + pseudoDeviceAddress() : "") +
(deviceName() != null ? ",name=" + deviceName() : "") +
(model() != null ? ",model=" + model() : "") +
"]";
}
public BLEDevice(TargetIdentifier identifier, DeviceDelegate delegate) {
super(identifier);
this.delegate = delegate;
}
public PseudoDeviceAddress pseudoDeviceAddress() {
return pseudoDeviceAddress;
}
public void pseudoDeviceAddress(PseudoDeviceAddress pseudoDeviceAddress) {
if (this.pseudoDeviceAddress == null || !this.pseudoDeviceAddress.equals(pseudoDeviceAddress)) {
this.pseudoDeviceAddress = pseudoDeviceAddress;
lastUpdatedAt = new Date();
}
}
//设备状态
public DeviceState state() {
return state;
}
public void state(DeviceState state) {
this.state = state;
lastUpdatedAt = new Date();
if (state == DeviceState.connected) {
lastConnectedAt = lastUpdatedAt;
}
delegate.device(this, DeviceAttribute.state);
}
public DeviceOperatingSystem operatingSystem() {
return operatingSystem;
}
public void operatingSystem(DeviceOperatingSystem operatingSystem) {
lastUpdatedAt = new Date();
// 设置忽略时间戳
if (operatingSystem == DeviceOperatingSystem.ignore) {
if (ignoreForDuration == null) {
ignoreForDuration = TimeInterval.minute;
} else if (ignoreForDuration.value < TimeInterval.minutes(3).value) {
ignoreForDuration = new TimeInterval(Math.round(ignoreForDuration.value * 1.2));
}
ignoreUntil = new Date(lastUpdatedAt.getTime() + ignoreForDuration.millis());
} else {
ignoreUntil = null;
}
//如果已确认操作系统,则重置持续时间和请求计数的忽略
if (operatingSystem == DeviceOperatingSystem.ios || operatingSystem == DeviceOperatingSystem.android) {
ignoreForDuration = null;
}
//设置操作系统
if (this.operatingSystem != operatingSystem) {
this.operatingSystem = operatingSystem;
delegate.device(this, DeviceAttribute.operatingSystem);
}
}
/// 时间判断之后忽略该设备
public boolean ignore() {
if (ignoreUntil == null) {
return false;
}
if (new Date().getTime() < ignoreUntil.getTime()) {
return true;
}
return false;
}
//外围设备信息
public BluetoothDevice peripheral() {
return peripheral;
}
public void peripheral(BluetoothDevice peripheral) {
if (this.peripheral != peripheral) {
this.peripheral = peripheral;
lastUpdatedAt = new Date();
}
}
public void immediateSendData(Data immediateSendData) {
this.immediateSendData = immediateSendData;
}
public Data immediateSendData() {
return immediateSendData;
}
public RSSI rssi() {
return rssi;
}
public void rssi(RSSI rssi) {
this.rssi = rssi;
lastUpdatedAt = new Date();
delegate.device(this, DeviceAttribute.rssi);
}
public void legacyPayloadCharacteristic(BluetoothGattCharacteristic characteristic) {
this.legacyPayloadCharacteristic = characteristic;
}
public BluetoothGattCharacteristic legacyPayloadCharacteristic() {
return legacyPayloadCharacteristic;
}
public TxPower txPower() {
return txPower;
}
public void txPower(TxPower txPower) {
this.txPower = txPower;
lastUpdatedAt = new Date();
delegate.device(this, DeviceAttribute.txPower);
}
public PayloadData payloadData() {
return payloadData;
}
public void payloadData(PayloadData payloadData) {
this.payloadData = payloadData;
lastPayloadDataUpdate = new Date();
lastUpdatedAt = lastPayloadDataUpdate;
delegate.device(this, DeviceAttribute.payloadData);
}
public TimeInterval timeIntervalSinceLastPayloadDataUpdate() {
if (lastPayloadDataUpdate == null) {
return TimeInterval.never;
}
return new TimeInterval((new Date().getTime() - lastPayloadDataUpdate.getTime()) / 1000);
}
public Calibration calibration() {
if (txPower == null) {
return null;
}
return new Calibration(CalibrationMeasurementUnit.BLETransmitPower, new Double(txPower.value));
}
public boolean receiveOnly() {
return receiveOnly;
}
public void receiveOnly(boolean receiveOnly) {
this.receiveOnly = receiveOnly;
lastUpdatedAt = new Date();
}
public void invalidateCharacteristics() {
signalCharacteristic = null;
payloadCharacteristic = null;
modelCharacteristic = null;
deviceNameCharacteristic = null;
legacyPayloadCharacteristic = null;
}
public BluetoothGattCharacteristic signalCharacteristic() {
return signalCharacteristic;
}
public void signalCharacteristic(BluetoothGattCharacteristic characteristic) {
this.signalCharacteristic = characteristic;
lastUpdatedAt = new Date();
}
public BluetoothGattCharacteristic payloadCharacteristic() {
return payloadCharacteristic;
}
public void payloadCharacteristic(BluetoothGattCharacteristic characteristic) {
this.payloadCharacteristic = characteristic;
lastUpdatedAt = new Date();
}
public boolean supportsModelCharacteristic() { return null != modelCharacteristic; }
public BluetoothGattCharacteristic modelCharacteristic() { return modelCharacteristic; }
public void modelCharacteristic(BluetoothGattCharacteristic modelCharacteristic) {
this.modelCharacteristic = modelCharacteristic;
lastUpdatedAt = new Date();
}
public boolean supportsDeviceNameCharacteristic() { return null != deviceNameCharacteristic; }
public BluetoothGattCharacteristic deviceNameCharacteristic() { return deviceNameCharacteristic; }
public void deviceNameCharacteristic(BluetoothGattCharacteristic deviceNameCharacteristic) {
this.deviceNameCharacteristic = deviceNameCharacteristic;
lastUpdatedAt = new Date();
}
public String deviceName() { return deviceName; }
public void deviceName(String deviceName) {
this.deviceName = deviceName;
lastUpdatedAt = new Date();
}
public String model() { return model; }
public void model(String model) {
this.model = model;
lastUpdatedAt = new Date();
}
public void registerDiscovery() {
lastDiscoveredAt = new Date();
lastUpdatedAt = lastDiscoveredAt;
}
public void registerWritePayload() {
lastUpdatedAt = new Date();
lastWritePayloadAt = lastUpdatedAt;
}
public TimeInterval timeIntervalSinceLastWritePayload() {
if (lastWritePayloadAt == null) {
return TimeInterval.never;
}
return new TimeInterval((new Date().getTime() - lastWritePayloadAt.getTime()) / 1000);
}
public void registerWriteRssi() {
lastUpdatedAt = new Date();
lastWriteRssiAt = lastUpdatedAt;
}
public TimeInterval timeIntervalSinceLastWriteRssi() {
if (lastWriteRssiAt == null) {
return TimeInterval.never;
}
return new TimeInterval((new Date().getTime() - lastWriteRssiAt.getTime()) / 1000);
}
public void registerWritePayloadSharing() {
lastUpdatedAt = new Date();
lastWritePayloadSharingAt = lastUpdatedAt;
}
public TimeInterval timeIntervalSinceLastWritePayloadSharing() {
if (lastWritePayloadSharingAt == null) {
return TimeInterval.never;
}
return new TimeInterval((new Date().getTime() - lastWritePayloadSharingAt.getTime()) / 1000);
}
public TimeInterval timeIntervalUntilIgnoreExpires() {
if (ignoreUntil == null) {
return TimeInterval.zero;
}
if (ignoreUntil.getTime() == Long.MAX_VALUE) {
return TimeInterval.never;
}
return new TimeInterval((ignoreUntil.getTime() - new Date().getTime()) / 1000);
}
public boolean protocolIsPioneer() {
return signalCharacteristic != null && payloadCharacteristic != null;
}
public void scanRecord(ScanRecord scanRecord) {
this.scanRecord = scanRecord;
}
public ScanRecord scanRecord() {
return scanRecord;
}
public boolean protocolIsLegacy() {
return legacyPayloadCharacteristic != null && signalCharacteristic == null;
}
@Override
public String toString() {
return description();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/BLESensor.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.Sensor;
public interface BLESensor extends Sensor {
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/BluetoothStateManager.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public interface BluetoothStateManager {
Queue delegates = new ConcurrentLinkedQueue<>();
BluetoothState state();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/BluetoothStateManagerDelegate.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
public interface BluetoothStateManagerDelegate {
void bluetoothStateManager(BluetoothState didUpdateState);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/Configurations.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.datatype.RandomSource;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.util.UUID;
/// 定义BLE传感器配置数据,例如 服务和特征性UUID
public class Configurations {
// BLE服务和特征性UUID,以及制造商ID
// Beacon: Service UUID。这是一个固定的UUID,使iOS设备即使在后台模式下也可以找到彼此。
// Android设备将需要首先使用制造商代码找到Apple设备,然后发现服务以识别实际的信标。
// -Service and characteristic UUIDs 是V4 UUID,它们是通过进行网络搜索以确保不返回任何结果而随机生成并经过测试的唯一性。
// 默认的Pioneer需要的UUID是 428132af-4746-42d3-801e-4572d65bfd9b
// -通过在基本UUID中设置值xxxx切换到16位UUID 0000xxxx-0000-1000-8000-00805F9B34FB
public static UUID serviceUUID = UUID.fromString("9ea51088-5add-438b-a269-dd6289844034");
// 用于android控制外围设备和中央设备之间连接的Signal Characteristic,例如:使彼此保持暂停状态
// -Characteristic UUID是随机生成的V4 UUID,已通过进行网络搜索来测试其唯一性,以确保其不返回任何结果。
public final static UUID androidSignalCharacteristicUUID = UUID.fromString("0ff27076-1fb2-4551-9670-7b468af8a9e7");
// 用于ios控制外围设备和中央设备之间连接的Signal Characteristic,例如:使彼此保持暂停状态
// -Characteristic UUID是随机生成的V4 UUID,已通过进行网络搜索来测试其唯一性,以确保其不返回任何结果。
public final static UUID iosSignalCharacteristicUUID = UUID.fromString("64451d23-b364-4967-bb43-51b82a56f3a5");
// 主要payload Characteristic UUID(读取),用于将有效载荷数据从外围设备分配到中央,例如:身份数据
// -特征性UUID是随机生成的V4 UUID,已通过进行网络搜索来测试其唯一性,以确保其不返回任何结果。
public final static UUID payloadCharacteristicUUID = UUID.fromString("2b5362a6-c995-4ab7-b744-cb366d4785bd");
/// 标准蓝牙服务和特征
/// 这些都是BLE标准中的固定UUID。
/// 通用访问服务的标准蓝牙Service UUID
/// -BLE标准的Service UUID
public final static UUID bluetoothGenericAccessServiceUUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
/// 通用访问服务的标准蓝牙Characteristic UUID:设备名称
/// - BLE标准中的Characteristic UUID
public final static UUID bluetoothGenericAccessServiceDeviceNameCharacteristicUUID = UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
/// 通用访问服务的标准蓝牙Characteristic UUID:设备名称
/// - BLE标准中的Characteristic UUID
public final static UUID bluetoothGenericAccessServiceAppearanceCharacteristicUUID = UUID.fromString("00002a01-0000-1000-8000-00805f9b34fb");
/// 设备信息服务的标准蓝牙Service UUID
/// - BLE标准中的Service UUID
public final static UUID bluetoothDeviceInformationServiceUUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb");
/// 设备信息服务的标准蓝牙Characteristic UUID:Model
/// - BLE标准中的Characteristic UUID
public final static UUID bluetoothDeviceInformationServiceModelCharacteristicUUID = UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb");
/// 设备信息服务的标准蓝牙Characteristic UUID: 制造商数据
/// - BLE标准中的Characteristic UUID
public final static UUID bluetoothDeviceInformationServiceManufacturerCharacteristicUUID = UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb");
/// 设备信息服务的标准蓝牙Characteristic UUID: TX Power
/// - BLE标准中的Characteristic UUID
public final static UUID bluetoothDeviceInformationServiceTxPowerCharacteristicUUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb");
/// 在Android上使用制造商数据来存储伪设备地址
/// -临时设定的ID
public final static int manufacturerIdForSensor = 65530;
/// BLE广告Apple的制造商ID,用于扫描后台iOS设备
public final static int manufacturerIdForApple = 76;
// BLE signal characteristic操作码
/// Signal characteristic用于写入有效负载的动作码,1字节动作代码 + 2字节小段int16的整型数据(payload长度)+payload数据
public final static byte signalCharacteristicActionWritePayload = (byte) 1;
/// /// Signal characteristic用于写入RSSI的动作码,1字节动作代码 + 4字节小段int32的RSSI数据
public final static byte signalCharacteristicActionWriteRSSI = (byte) 2;
/// Signal characteristic用于写入共享payload的动作码,1字节动作代码 + 2字节小段int16的整型数据(payload长度)+payload sharing数据
public final static byte signalCharacteristicActionWritePayloadSharing = (byte) 3;
/// 立即写操作码
public final static byte signalCharacteristicActionWriteImmediate = (byte) 4;
// 应用程序可配置BLE功能
/// 除了默认的Pioneer通信过程之外,还定期更新payload数据
/// - 使用此选项可根据应用程序payload寿命启用常规payload读取。
/// - 设置为.never以禁用此功能。
/// - 有效负载更新将以didRead的形式报告给SensorDelegate。
/// - 设置立即生效,无需重新启动BLESensor,也可以在BLESensor处于活动状态时应用。
public static TimeInterval payloadDataUpdateTimeInterval = TimeInterval.never;
// 过滤重复payload数据并抑制传感器代理响应(didRead:fromTarget)
/// - 设置为.never以禁用此功能。
/// - 设置时间间隔N以过滤最近N秒内看到的重复有效载荷数据
/// - 例如:60表示在最后一分钟过滤重复项
/// - 从所有目标过滤所有出现的payload数据
public static TimeInterval filterDuplicatePayloadData = TimeInterval.never;
// 共享有效负载的到期时间,以确保仅共享最近看到的有效负载
public static TimeInterval payloadSharingExpiryTimeInterval = new TimeInterval(5 * TimeInterval.minute.value);
/// 广播刷新时间间隔
public static TimeInterval advertRefreshTimeInterval = TimeInterval.minutes(15);
/// 用于生成伪设备地址的随机化方法,有关详细信息,请参见PseudoDeviceAddress和RandomSource文件。
/// - 设置为Random以获得可靠的连续运行
/// - 其他方法将在4-8小时后导致阻塞并在空闲设备上中断操作
/// - 阻塞会在 应用初始化,广告刷新和影响系统服务时 发生
public static RandomSource.Method pseudoDeviceAddressRandomisation = RandomSource.Method.Random;
public static boolean deviceIntrospectionEnabled = false;
public static boolean deviceFilterTrainingEnabled = false;
/// 根据消息模式定义设备过滤规则
/// - 避免连接到无法托管传感器服务的设备
/// - 与广告中的每个制造商特定的数据消息(十六进制格式)匹配
/// - Java正则表达式模式,不区分大小写,可在消息中的任何位置找到模式
/// - 切记要在消息开头添加^以进行匹配
/// - 在开发环境中使用deviceFilterTrainingEnabled来识别模式
public static String[] deviceFilterFeaturePatterns = new String[]{
"^10....04",
"^10....14",
"^0100000000000000000000000000000000",
"^05","^07","^09",
"^00","^1002","^06","^08","^03","^0C","^0D","^0F","^0E","^0B"
};
/// 启用惯性传感器
public static boolean inertiaSensorEnabled = false;
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/Database.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanResult;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadSharingData;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.List;
// 用于整理来自异步BLE操作的信息片段的注册表。
public interface Database {
// 添加用于处理数据库事件的委托
void add(BLEDatabaseDelegate delegate);
// 获取或创建用于整理来自异步BLE操作的信息的设备。
BLEDevice device(ScanResult scanResult);
// 获取或创建用于整理来自异步BLE操作的信息的设备。
BLEDevice device(BluetoothDevice bluetoothDevice);
/// 获取或创建用于整理来自异步BLE操作的信息的设备。
BLEDevice device(PayloadData payloadData);
/// 从TargetIdentifier获取设备
BLEDevice device(TargetIdentifier targetIdentifier);
/// 获取所有设备
List devices();
/// 删除设备
void delete(BLEDevice device);
/// 获取对等方的有效负载共享数据
PayloadSharingData payloadSharingData(BLEDevice peer);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/DeviceAttribute.java
================================================
package com.ABC.pioneer.sensor.ble;
//BLE设备属性
public enum DeviceAttribute {
peripheral, state, operatingSystem, payloadData, rssi, txPower
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/DeviceDelegate.java
================================================
package com.ABC.pioneer.sensor.ble;
public interface DeviceDelegate {
void device(BLEDevice device, DeviceAttribute didUpdate);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/DeviceOperatingSystem.java
================================================
package com.ABC.pioneer.sensor.ble;
public enum DeviceOperatingSystem {
android_tbc, android, ios_tbc, ios, ignore, shared, unknown
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/DeviceState.java
================================================
package com.ABC.pioneer.sensor.ble;
/// BLE设备连接状态
public enum DeviceState {
connecting, connected, disconnected
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/DeviceUpdatedComparator.java
================================================
package com.ABC.pioneer.sensor.ble;
import java.util.Comparator;
//设备更新比较器
//这里我们按最后一次更新的时间对device进行排序,以便于后续的操作
public class DeviceUpdatedComparator implements Comparator {
public int compare(BLEDevice a, BLEDevice b)
{
// 最后更新时间的降序(因此逻辑相反)
long bt = b.lastUpdatedAt.getTime();
long at = a.lastUpdatedAt.getTime();
if (bt > at) {
return 1;
}
if (bt < at) {
return -1;
}
return 0;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/Receiver.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.Sensor;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
//receiver扫描具有固定Service UUID 的外围设备
public interface Receiver extends Sensor {
Queue delegates = new ConcurrentLinkedQueue<>();
/// 立即发送数据。
boolean immediateSend(Data data, TargetIdentifier targetIdentifier);
// 立即发送给所有人(已连接/最近/附近)
boolean immediateSendAll(Data data);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/SpecificBLESensor.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.content.Context;
import com.ABC.pioneer.sensor.PayloadSupplier;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.ProximityMeasurementUnit;
import com.ABC.pioneer.sensor.datatype.RSSI;
import com.ABC.pioneer.sensor.datatype.SensorState;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.util.Date;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SpecificBLESensor implements BLESensor, BLEDatabaseDelegate, BluetoothStateManagerDelegate {
private final Queue delegates = new ConcurrentLinkedQueue<>();
private final Transmitter transmitter;
private final Receiver receiver;
private final ExecutorService operationQueue = Executors.newSingleThreadExecutor();
// 记录有效负载数据以启用重复数据删除
private final Map didReadPayloadData = new ConcurrentHashMap<>();
public SpecificBLESensor(Context context, PayloadSupplier payloadSupplier) {
final BluetoothStateManager bluetoothStateManager = new SpecificBluetoothStateManager(context);
final Database database = new SpecificDatabase();
final Timer timer = new Timer(context);
bluetoothStateManager.delegates.add(this);
transmitter = new SpecificTransmitter(context, bluetoothStateManager, timer, payloadSupplier, database);
receiver = new SpecificReceiver(context, bluetoothStateManager, timer, database, transmitter, payloadSupplier);
database.add(this);
}
@Override
public void add(SensorDelegate delegate) {
delegates.add(delegate);
transmitter.add(delegate);
receiver.add(delegate);
}
@Override
public void start() {
transmitter.start();
receiver.start();
}
@Override
public void stop() {
transmitter.stop();
receiver.stop();
}
// BLEDatabaseDelegate
@Override
public void bleDatabaseDidUpdate(final BLEDevice device, DeviceAttribute attribute) {
switch (attribute) {
case rssi: {
final RSSI rssi = device.rssi();
if (rssi == null) {
return;
}
final Proximity proximity = new Proximity(ProximityMeasurementUnit.RSSI, (double) rssi.value, device.calibration());
operationQueue.execute(new Runnable() {
@Override
public void run() {
for (SensorDelegate delegate : delegates) {
delegate.sensor(SensorType.BLE, proximity, device.identifier);
}
}
});
final PayloadData payloadData = device.payloadData();
if (payloadData == null) {
return;
}
break;
}
case payloadData: {
final PayloadData payloadData = device.payloadData();
if (payloadData == null) {
return;
}
// 最近一次对有效负载进行重复数据删除
if (Configurations.filterDuplicatePayloadData != TimeInterval.never) {
final long removePayloadDataBefore = new Date().getTime() - Configurations.filterDuplicatePayloadData.millis();
for (Map.Entry entry : didReadPayloadData.entrySet()) {
if (entry.getValue().getTime() < removePayloadDataBefore) {
didReadPayloadData.remove(entry.getKey());
}
}
final Date lastReportedAt = didReadPayloadData.get(payloadData);
if (lastReportedAt != null) {
return;
}
didReadPayloadData.put(payloadData, new Date());
}
// 通知delegates
operationQueue.execute(new Runnable() {
@Override
public void run() {
for (SensorDelegate delegate : delegates) {
delegate.sensor(SensorType.BLE, payloadData, device.identifier);
}
}
});
break;
}
default: {
}
}
}
public boolean immediateSend(Data data, TargetIdentifier targetIdentifier) {
return receiver.immediateSend(data, targetIdentifier);
}
public boolean immediateSendAll(Data data) {
return receiver.immediateSendAll(data);
}
@Override
public void bleDatabaseDidCreate(final BLEDevice device) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
for (SensorDelegate delegate : delegates) {
delegate.sensor(SensorType.BLE, device.identifier);
}
}
});
}
@Override
public void bleDatabaseDidDelete(BLEDevice device) {
}
// BluetoothStateManagerDelegate
@Override
public void bluetoothStateManager(BluetoothState didUpdateState) {
SensorState sensorState = SensorState.off;
if (didUpdateState == BluetoothState.poweredOn) {
sensorState = SensorState.on;
} else if (didUpdateState == BluetoothState.unsupported) {
sensorState = SensorState.unavailable;
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/SpecificBluetoothStateManager.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
/**
* 监测蓝牙状态是否变化
*/
public class SpecificBluetoothStateManager implements BluetoothStateManager {
private BluetoothState state = null;
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
try {
final int nativeState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch (nativeState) {
case BluetoothAdapter.STATE_ON:
state = BluetoothState.poweredOn;
for (BluetoothStateManagerDelegate delegate : delegates) {
delegate.bluetoothStateManager(BluetoothState.poweredOn);
}
break;
case BluetoothAdapter.STATE_OFF:
state = BluetoothState.poweredOff;
for (BluetoothStateManagerDelegate delegate : delegates) {
delegate.bluetoothStateManager(BluetoothState.poweredOff);
}
break;
}
} catch (Throwable e) {
}
}
}
};
/**
* 监测蓝牙状态是否变化
*/
public SpecificBluetoothStateManager(Context context) {
state = state();
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
context.registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public BluetoothState state() {
if (state == null) {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
state = BluetoothState.unsupported;
return state;
}
switch (BluetoothAdapter.getDefaultAdapter().getState()) {
case BluetoothAdapter.STATE_ON:
state = BluetoothState.poweredOn;
break;
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_TURNING_ON:
default:
state = BluetoothState.poweredOff;
break;
}
}
return state;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/SpecificDatabase.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadSharingData;
import com.ABC.pioneer.sensor.datatype.PseudoDeviceAddress;
import com.ABC.pioneer.sensor.datatype.RSSI;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SpecificDatabase implements Database, DeviceDelegate {
private final Queue delegates = new ConcurrentLinkedQueue<>();
private final Map database = new ConcurrentHashMap<>();
private final ExecutorService queue = Executors.newSingleThreadExecutor();
/// 获取Android设备的伪设备地址
private PseudoDeviceAddress pseudoDeviceAddress(final ScanResult scanResult) {
final ScanRecord scanRecord = scanResult.getScanRecord();
if (scanRecord == null) {
return null;
}
// Pioneer伪设备地址
if (scanRecord.getManufacturerSpecificData(Configurations.manufacturerIdForSensor) != null) {
final byte[] data = scanRecord.getManufacturerSpecificData(Configurations.manufacturerIdForSensor);
if (data != null && data.length == 6) {
return new PseudoDeviceAddress(data);
}
}
// 未找到
return null;
}
@Override
public void add(final BLEDatabaseDelegate delegate) {
delegates.add(delegate);
}
@Override
public BLEDevice device(final TargetIdentifier targetIdentifier) {
return database.get(targetIdentifier);
}
@Override
public BLEDevice device(final BluetoothDevice bluetoothDevice) {
final TargetIdentifier identifier = new TargetIdentifier(bluetoothDevice);
BLEDevice device = database.get(identifier);
if (device == null) {
final BLEDevice newDevice = new BLEDevice(identifier, this);
device = newDevice;
database.put(identifier, newDevice);
queue.execute(new Runnable() {
@Override
public void run() {
for (BLEDatabaseDelegate delegate : delegates) {
delegate.bleDatabaseDidCreate(newDevice);
}
}
});
}
device.peripheral(bluetoothDevice);
return device;
}
@Override
public BLEDevice device(final ScanResult scanResult) {
// 通过目标标识符获取设备
final BluetoothDevice bluetoothDevice = scanResult.getDevice();
final TargetIdentifier targetIdentifier = new TargetIdentifier(bluetoothDevice);
final BLEDevice existingDevice = database.get(targetIdentifier);
if (existingDevice != null) {
return existingDevice;
}
// 通过伪设备地址获取设备信息
final PseudoDeviceAddress pseudoDeviceAddress = pseudoDeviceAddress(scanResult);
if (pseudoDeviceAddress != null) {
// 重用现有的Android设备
BLEDevice deviceWithSamePseudoDeviceAddress = null;
for (final BLEDevice device : database.values()) {
if (device.pseudoDeviceAddress() != null && device.pseudoDeviceAddress().equals(pseudoDeviceAddress)) {
deviceWithSamePseudoDeviceAddress = device;
break;
}
}
if (deviceWithSamePseudoDeviceAddress != null) {
database.put(targetIdentifier, deviceWithSamePseudoDeviceAddress);
if (deviceWithSamePseudoDeviceAddress.peripheral() != bluetoothDevice) {
deviceWithSamePseudoDeviceAddress.peripheral(bluetoothDevice);
}
if (deviceWithSamePseudoDeviceAddress.operatingSystem() != DeviceOperatingSystem.android) {
deviceWithSamePseudoDeviceAddress.operatingSystem(DeviceOperatingSystem.android);
}
return deviceWithSamePseudoDeviceAddress;
}
// 创建新的Android设备
else {
final BLEDevice newDevice = device(bluetoothDevice);
newDevice.pseudoDeviceAddress(pseudoDeviceAddress);
newDevice.operatingSystem(DeviceOperatingSystem.android);
return newDevice;
}
}
// 创建新的设备
return device(bluetoothDevice);
}
@Override
public List devices() {
return new ArrayList<>(database.values());
}
@Override
public BLEDevice device(PayloadData payloadData) {
BLEDevice device = null;
for (BLEDevice candidate : database.values()) {
if (payloadData.equals(candidate.payloadData())) {
device = candidate;
break;
}
}
if (device == null) {
final TargetIdentifier identifier = new TargetIdentifier();
final BLEDevice newDevice = new BLEDevice(identifier, this);
device = newDevice;
database.put(identifier, newDevice);
queue.execute(new Runnable() {
@Override
public void run() {
for (BLEDatabaseDelegate delegate : delegates) {
delegate.bleDatabaseDidCreate(newDevice);
}
}
});
}
device.payloadData(payloadData);
return device;
}
@Override
public void delete(final BLEDevice device) {
if (device == null) {
return;
}
final List identifiers = new ArrayList<>();
for (final Map.Entry entry : database.entrySet()) {
if (entry.getValue() == device) {
identifiers.add(entry.getKey());
}
}
if (identifiers.isEmpty()) {
return;
}
for (final TargetIdentifier identifier : identifiers) {
database.remove(identifier);
}
queue.execute(new Runnable() {
@Override
public void run() {
for (final BLEDatabaseDelegate delegate : delegates) {
delegate.bleDatabaseDidDelete(device);
}
}
});
}
// BLEDeviceDelegate
@Override
public void device(final BLEDevice device, final DeviceAttribute didUpdate) {
queue.execute(new Runnable() {
@Override
public void run() {
for (BLEDatabaseDelegate delegate : delegates) {
delegate.bleDatabaseDidUpdate(device, didUpdate);
}
}
});
}
@Override
public PayloadSharingData payloadSharingData(final BLEDevice peer) {
final RSSI rssi = peer.rssi();
if (rssi == null) {
return new PayloadSharingData(new RSSI(127), new Data(new byte[0]));
}
// 获取此设备最近查看过的其他设备
final List unknownDevices = new ArrayList<>();
final List knownDevices = new ArrayList<>();
for (BLEDevice device : database.values()) {
// 最近看过的设备信息
if (device.timeIntervalSinceLastUpdate().value >= Configurations.payloadSharingExpiryTimeInterval.value) {
continue;
}
// 设备含有payload数据
if (device.payloadData() == null) {
continue;
}
// 设备是IOS设备或者是receive-only(J16)设备
if (!(device.operatingSystem() == DeviceOperatingSystem.ios || device.receiveOnly())) {
continue;
}
// 设备是Pioneer设备
if (device.signalCharacteristic() == null) {
continue;
}
// payload数据不是连接方的数据
if (peer.payloadData() != null && (Arrays.equals(device.payloadData().value, peer.payloadData().value))) {
continue;
}
// 对于连接方来说,payload是新的payload
if (peer.payloadSharingData.contains(device.payloadData())) {
knownDevices.add(device);
} else {
unknownDevices.add(device);
}
}
// 最近看到的未知设备优先
final List devices = new ArrayList<>();
Collections.sort(unknownDevices, new Comparator() {
@Override
public int compare(BLEDevice d0, BLEDevice d1) {
return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime());
}
});
Collections.sort(knownDevices, new Comparator() {
@Override
public int compare(BLEDevice d0, BLEDevice d1) {
return Long.compare(d1.lastUpdatedAt.getTime(), d0.lastUpdatedAt.getTime());
}
});
devices.addAll(unknownDevices);
devices.addAll(knownDevices);
if (devices.size() == 0) {
return new PayloadSharingData(new RSSI(127), new Data(new byte[0]));
}
// 限制共享量,以避免通过BLE传输过多的数据
// 根据规范限制为512个字节,响应为510个字节,iOS需要响应
final Set sharedPayloads = new HashSet<>(devices.size());
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
for (BLEDevice device : devices) {
final PayloadData payloadData = device.payloadData();
if (payloadData == null) {
continue;
}
// 消除重复(当同一设备更改地址但旧版本尚未过期时,会发生这种情况)
if (sharedPayloads.contains(payloadData)) {
continue;
}
// 通过BLE传输限制限制有效负载共享
if (payloadData.value.length + byteArrayOutputStream.toByteArray().length > 510) {
break;
}
try {
byteArrayOutputStream.write(payloadData.value);
peer.payloadSharingData.add(payloadData);
sharedPayloads.add(payloadData);
} catch (Throwable e) {
}
}
final Data data = new Data(byteArrayOutputStream.toByteArray());
return new PayloadSharingData(rssi, data);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/SpecificReceiver.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
import com.ABC.pioneer.sensor.PayloadSupplier;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.analysis.Sample;
import com.ABC.pioneer.sensor.ble.filter.BLEDeviceFilter;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
import com.ABC.pioneer.sensor.datatype.Callback;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadSharingData;
import com.ABC.pioneer.sensor.datatype.RSSI;
import com.ABC.pioneer.sensor.datatype.SignalCharacteristicData;
import com.ABC.pioneer.sensor.datatype.SignalCharacteristicDataType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class SpecificReceiver extends BluetoothGattCallback implements Receiver {
// 扫描 开/关/过程 持续时间
private final static long scanOnDurationMillis = TimeInterval.seconds(4).millis();
private final static long scanRestDurationMillis = TimeInterval.seconds(1).millis();
private final static long scanProcessDurationMillis = TimeInterval.seconds(60).millis();
private final static long scanOffDurationMillis = TimeInterval.seconds(2).millis();
private final static long timeToConnectDeviceLimitMillis = TimeInterval.seconds(12).millis();
private final static Sample timeToConnectDevice = new Sample();
private final static Sample timeToProcessDevice = new Sample();
private final static int defaultMTU = 20;
private final Context context;
private final BluetoothStateManager bluetoothStateManager;
private final Database database;
private final Transmitter transmitter;
private final PayloadSupplier payloadSupplier;
private final BLEDeviceFilter deviceFilter;
private final ExecutorService operationQueue = Executors.newSingleThreadExecutor();
private final Queue scanResults = new ConcurrentLinkedQueue<>();
private final AtomicBoolean receiverEnabled = new AtomicBoolean(false);
private enum NextTask {
nothing, readPayload, writePayload, writeRSSI, writePayloadSharing, immediateSend,
readModel, readDeviceName
}
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult scanResult) {
scanResults.add(scanResult);
// 在数据库中创建或更新设备
final BLEDevice device = database.device(scanResult);
device.registerDiscovery();
// 从扫描结果中读取RSSI
device.rssi(new RSSI(scanResult.getRssi()));
}
@Override
public void onBatchScanResults(List results) {
for (ScanResult scanResult : results) {
onScanResult(0, scanResult);
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
/**
* 启用蓝牙后,Receiver会自动启动。
*/
public SpecificReceiver(Context context, BluetoothStateManager bluetoothStateManager, Timer timer, Database database, Transmitter transmitter, PayloadSupplier payloadSupplier) {
this.context = context;
this.bluetoothStateManager = bluetoothStateManager;
this.database = database;
this.transmitter = transmitter;
this.payloadSupplier = payloadSupplier;
timer.add(new ScanLoopTask());
if (Configurations.deviceFilterTrainingEnabled) {
Configurations.deviceIntrospectionEnabled = true;
Configurations.payloadDataUpdateTimeInterval = TimeInterval.minute;
this.deviceFilter = new BLEDeviceFilter(context, "filter.csv");
} else {
this.deviceFilter = new BLEDeviceFilter();
}
}
// BLEReceiver
@Override
public void add(SensorDelegate delegate) {
delegates.add(delegate);
}
@Override
public void start() {
if (receiverEnabled.compareAndSet(false, true)) {
} else {
}
}
@Override
public void stop() {
if (receiverEnabled.compareAndSet(true, false)) {
} else {
}
}
@Override
public boolean immediateSend(Data data, TargetIdentifier targetIdentifier) {
BLEDevice device = database.device(targetIdentifier);
if (null == device) {
return false;
}
final Data dataToSend = SignalCharacteristicData.encodeImmediateSend(new ImmediateSendData(data));
// 立即发送过程
// 1. 设置设备的立即发送数据
// 2. 启动与设备的连接
// 3. onConnectionStateChange()将触发服务和特征发现
// 4. signalCharacteristic发现将触发nextTask()
// 5. 如果已为设备设置了立即发送数据,则nextTask()将为.immediateSend
// 6. 将调用writeSignalCharacteristic()执行立即发送到signalCharacteristic
// 7. 写入信号数据后将触发onCharacteristicWrite()
// 8. 写入完成后,设备的立即发送数据将设置为null
// 9. 连接立即关闭
device.immediateSendData(dataToSend);
return taskConnectDevice(device);
}
@Override
public boolean immediateSendAll(Data data) {
// 编码数据
final Data dataToSend = SignalCharacteristicData.encodeImmediateSend(new ImmediateSendData(data));
// 按看到时间的降序排列(最新的优先)
// 选择Targets
SortedSet targets = new TreeSet<>(new DeviceUpdatedComparator());
// 在最后一分钟获取看到的目标(通过广告获取RSSI)
for (BLEDevice device : database.devices()) {
if (!device.ignore() && device.signalCharacteristic() != null && device.timeIntervalSinceLastUpdate().value < 60) {
targets.add(device);
}
}
// 发送信息
// 连接并立即发送给每个设备
// NOTE: 这个单独的循环还没有排序交互。 一旦工作,重构就可以了。
for (BLEDevice target : targets) {
target.immediateSendData(dataToSend);
}
// 现在强制执行乱序连接(并因此立即发送作为下一个操作)
for (BLEDevice target : targets) {
taskConnectDevice(target);
}
return true;
}
// 设定startScan-wait-stopScan-processScanResults-wait-repeat的扫描循环
private enum ScanLoopState {
scanStarting, scanStarted, scanStopping, scanStopped, processing, processed
}
private class ScanLoopTask implements TimerDelegate {
private ScanLoopState scanLoopState = ScanLoopState.processed;
private long lastStateChangeAt = System.currentTimeMillis();
private void state(final long now, ScanLoopState state) {
final long elapsed = now - lastStateChangeAt;
this.scanLoopState = state;
lastStateChangeAt = now;
}
private long timeSincelastStateChange(final long now) {
return now - lastStateChangeAt;
}
// 记录时间
@Override
public void bleTimer(final long now) {
switch (scanLoopState) {
case processed: {
if (receiverEnabled.get() && bluetoothStateManager.state() == BluetoothState.poweredOn) {
final long period = timeSincelastStateChange(now);
if (period >= scanOffDurationMillis) {
final BluetoothLeScanner bluetoothLeScanner = bluetoothLeScanner();
if (bluetoothLeScanner == null) {
return;
}
state(now, ScanLoopState.scanStarting);
startScan(bluetoothLeScanner, new Callback() {
@Override
public void accept(Boolean value) {
state(now, value ? ScanLoopState.scanStarted : ScanLoopState.scanStopped);
}
});
}
}
break;
}
case scanStarted: {
final long period = timeSincelastStateChange(now);
if (period >= scanOnDurationMillis) {
final BluetoothLeScanner bluetoothLeScanner = bluetoothLeScanner();
if (bluetoothLeScanner == null) {
return;
}
state(now, ScanLoopState.scanStopping);
stopScan(bluetoothLeScanner, new Callback() {
@Override
public void accept(Boolean value) {
state(now, ScanLoopState.scanStopped);
}
});
}
break;
}
case scanStopped: {
if (bluetoothStateManager.state() == BluetoothState.poweredOn) {
final long period = timeSincelastStateChange(now);
if (period >= scanRestDurationMillis) {
state(now, ScanLoopState.processing);
processScanResults(new Callback() {
@Override
public void accept(Boolean value) {
state(now, ScanLoopState.processed);
if (!receiverEnabled.get()) {
}
}
});
}
}
break;
}
}
}
private BluetoothLeScanner bluetoothLeScanner() {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
return null;
}
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
if (bluetoothLeScanner == null) {
return null;
}
return bluetoothLeScanner;
}
}
/// 获取BLE扫描器并开始扫描
private void startScan(final BluetoothLeScanner bluetoothLeScanner, final Callback callback) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
try {
scanForPeripherals(bluetoothLeScanner);
if (callback != null) {
callback.accept(true);
}
} catch (Throwable e) {
if (callback != null) {
callback.accept(false);
}
}
}
});
}
/// 扫描广告传感器服务的设备以及所有Apple设备,如下所示:
// iOS后台广告不包含服务UUID。 传感器可能会花一些时间与没有重复运行传感器代码的Apple设备进行通信,
// 但是没有可靠的方法对此进行过滤,因为仅由于暂时性问题而可能会缺少该服务。
// 这将在taskConnect中处理。
private void scanForPeripherals(final BluetoothLeScanner bluetoothLeScanner) {
final List filter = new ArrayList<>(4);
// 在iOS(后台)设备上扫描Pioneer协议服务
filter.add(new ScanFilter.Builder().setManufacturerData(
Configurations.manufacturerIdForApple, new byte[0], new byte[0]).build());
// 在Android或iOS(前台)设备上扫描Pioneer协议服务
filter.add(new ScanFilter.Builder().setServiceUuid(
new ParcelUuid(Configurations.serviceUUID),
new ParcelUuid(new UUID(0xFFFFFFFFFFFFFFFFL, 0)))
.build());
final ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setReportDelay(0)
.build();
bluetoothLeScanner.startScan(filter, settings, scanCallback);
}
private void processScanResults(final Callback callback) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
try {
processScanResults();
} catch (Throwable e) {
callback.accept(false);
}
callback.accept(true);
}
});
}
/// 获取BLE扫描器并停止扫描 Get BLE scanner and stop scan
private void stopScan(final BluetoothLeScanner bluetoothLeScanner, final Callback callback) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
try {
bluetoothLeScanner.stopScan(scanCallback);
} catch (Throwable e) {
}
try {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
bluetoothAdapter.cancelDiscovery();
}
} catch (Throwable e) {
}
callback.accept(true);
}
});
}
// 处理扫描结果
/// 处理扫描结果。
private void processScanResults() {
final long t0 = System.currentTimeMillis();
// Identify devices discovered in last scan
final List didDiscover = didDiscover();
taskRemoveExpiredDevices();
taskCorrectConnectionStatus();
taskConnect(didDiscover);
final long t1 = System.currentTimeMillis();
}
// MARK:- didDiscover
/**
* 将扫描结果处理为...
* 1.从扫描结果中为新设备创建BLEDevice
* 2.阅读RSSI
* 3.尽可能确定操作系统
*/
private List didDiscover() {
// 取得可同时修改的扫描结果的当前副本
final List scanResultList = new ArrayList<>(scanResults.size());
while (scanResults.size() > 0) {
scanResultList.add(scanResults.poll());
}
// 处理扫描结果并返回在扫描结果中创建/更新的设备
final Set deviceSet = new HashSet<>();
final List devices = new ArrayList<>();
for (ScanResult scanResult : scanResultList) {
final BLEDevice device = database.device(scanResult);
if (deviceSet.add(device)) {
devices.add(device);
}
// 设置扫描记录
device.scanRecord(scanResult.getScanRecord());
// 设置发射功率
if (device.scanRecord() != null) {
int txPowerLevel = device.scanRecord().getTxPowerLevel();
if (txPowerLevel != Integer.MIN_VALUE) {
device.txPower(new TxPower(txPowerLevel));
}
}
// 尽可能从扫描记录中识别操作系统
// - 发现了Sensor service + Manufacturer是苹果 -> iOS (前台)
// - 发现了Sensor service + Manufacturer不是苹果 -> Android
// - 未发现Sensor service + Manufacturer是苹果 -> iOS (后台)或者是IOS未广播Sensor服务
// - 未发现Sensor service + Manufacturer不是苹果 -> Ignore
final boolean hasSensorService = hasSensorService(scanResult);
final boolean isAppleDevice = isAppleDevice(scanResult);
if (hasSensorService && isAppleDevice) {
// 绝对是前景模式下提供传感器服务的iOS设备
device.operatingSystem(DeviceOperatingSystem.ios);
} else if (hasSensorService) { // 隐含着!isAppleDevice
if (device.operatingSystem() != DeviceOperatingSystem.android) {
device.operatingSystem(DeviceOperatingSystem.android_tbc);
}
} else if (isAppleDevice) { // 隐含着!hasSensorService
final BLEDeviceFilter.MatchingPattern matchingPattern = deviceFilter.match(device);
if (device.operatingSystem() != DeviceOperatingSystem.ios && matchingPattern != null) {
device.operatingSystem(DeviceOperatingSystem.ignore);
}
// 可能是一台iOS设备在后台模式下提供传感器服务
// 无法确定是否在连接后再进行其他检查,因此仅在未知时才能设置操作系统以进行猜测。
if (device.operatingSystem() == DeviceOperatingSystem.unknown) {
device.operatingSystem(DeviceOperatingSystem.ios_tbc);
}
}else {
if (!(device.operatingSystem() == DeviceOperatingSystem.ios || device.operatingSystem() == DeviceOperatingSystem.android)) {
device.operatingSystem(DeviceOperatingSystem.ignore);
}
}
}
return devices;
}
// 信息清理
/// 删除超过15分钟未更新的设备,
// 因为UUID在超出范围20分钟后可能已更改,因此需要进行发现。
private void taskRemoveExpiredDevices() {
final List devicesToRemove = new ArrayList<>();
for (BLEDevice device : database.devices()) {
if (device.timeIntervalSinceLastUpdate().value > TimeInterval.minutes(15).value) {
devicesToRemove.add(device);
}
}
for (BLEDevice device : devicesToRemove) {
database.delete(device);
}
}
/// 连接保持的时间不应超过1分钟,可能没有收到onConnectionStateChange回调。
private void taskCorrectConnectionStatus() {
for (BLEDevice device : database.devices()) {
if (device.state() == DeviceState.connected && device.timeIntervalSinceConnected().value > TimeInterval.minute.value) {
device.state(DeviceState.disconnected);
}
}
}
/// 扫描结果是否表明该设备是Apple制造的?
private static boolean isAppleDevice(final ScanResult scanResult) {
final ScanRecord scanRecord = scanResult.getScanRecord();
if (scanRecord == null) {
return false;
}
final byte[] data = scanRecord.getManufacturerSpecificData(Configurations.manufacturerIdForApple);
return data != null;
}
/// 扫描结果是否包括用于传感器服务的广告?
private static boolean hasSensorService(final ScanResult scanResult) {
final ScanRecord scanRecord = scanResult.getScanRecord();
if (scanRecord == null) {
return false;
}
final List serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids == null || serviceUuids.size() == 0) {
return false;
}
for (ParcelUuid serviceUuid : serviceUuids) {
if (serviceUuid.getUuid().equals(Configurations.serviceUUID)) {
return true;
}
}
return false;
}
// 连接任务
private void taskConnect(final List discovered) {
// 连接优先级在这里毫无意义
// 因为三星A10和A20之类的设备会在每次扫描调用时更改mac地址,因此优化新设备的处理效率更高。
final long timeStart = System.currentTimeMillis();
int devicesProcessed = 0;
for (BLEDevice device : discovered) {
// 如果超过时间限制,则停止处理
final long elapsedTime = System.currentTimeMillis() - timeStart;
if (elapsedTime >= scanProcessDurationMillis) {
break;
}
if (devicesProcessed > 0) {
final long predictedElapsedTime = Math.round((elapsedTime / (double) devicesProcessed) * (devicesProcessed + 1));
if (predictedElapsedTime > scanProcessDurationMillis) {
break;
}
}
if (nextTaskForDevice(device) == NextTask.nothing) {
continue;
}
taskConnectDevice(device);
devicesProcessed++;
}
}
private boolean taskConnectDevice(final BLEDevice device) {
if (device.state() == DeviceState.connected) {
return true;
}
// 连接
final long timeConnect = System.currentTimeMillis();
device.state(DeviceState.connecting);
BluetoothGatt gatt = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// API 23及更高版本-仅强制低能耗
gatt = device.peripheral().connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE);
} else {
// 支持回到API 21
gatt = device.peripheral().connectGatt(context, false, this);
}
if (gatt == null) {
device.state(DeviceState.disconnected);
return false;
}
// 等待连接
// 连接请求通常应导致.connected或.disconnected状态,该状态由回调函数onConnectionStateChange()异步设置。
// 但是,由于BLE问题,某些连接可能会无限期地陷入.connecting状态,因此从不调用回调函数,从而使设备处于不确定状态。
// 这样,跟随循环将运行固定时间,以检查连接是否成功,否则中止连接以使设备处于一致的默认.disconnected状态。
while (device.state() != DeviceState.connected && device.state() != DeviceState.disconnected && (System.currentTimeMillis() - timeConnect) < timeToConnectDeviceLimitMillis) {
try {
Thread.sleep(200);
} catch (Throwable e) {
}
}
if (device.state() != DeviceState.connected) {
// 无法在时限内建立连接,假设连接失败并断开设备以使其处于一致的默认.disconnected状态
try {
gatt.close();
} catch (Throwable e) {
}
return false;
} else {
// 连接成功,记下建立连接的时间,以通知timeToConnectDeviceLimitMillis的设置。
// 先前的实现使用自适应算法来根据设备功能调整此参数,但是由于目标设备在确定连接时间中起着很大的作用,
// 并且由于环境的原因而无法预测,因此认为这对于获得最小的性能而言太不可靠了。
final long connectElapsed = System.currentTimeMillis() - timeConnect;
timeToConnectDevice.add(connectElapsed);
}
// 等待断开连接
// 此时设备已连接,并且所有实际工作都由该函数外部的回调方法异步执行。
// 因此,此时唯一需要做的工作就是跟踪连接时间,以确保连接保持的时间不会太长
// 如果连接保持的时间过长,则此函数将通过调用gatt.close()断开设备
// 以使其处于一致的默认.disconnected状态来强制断开连接。
while (device.state() != DeviceState.disconnected && (System.currentTimeMillis() - timeConnect) < scanProcessDurationMillis) {
try {
Thread.sleep(500);
} catch (Throwable e) {
}
}
boolean success = true;
// 超时连接(如果需要),并且始终将状态设置为断开
if (device.state() != DeviceState.disconnected) {
try {
gatt.close();
} catch (Throwable e) {
}
success = false;
}
// 最后始终将状态设置为.disconnected
device.state(DeviceState.disconnected);
final long timeDisconnect = System.currentTimeMillis();
final long timeElapsed = (timeDisconnect - timeConnect);
if (success) {
timeToProcessDevice.add(timeElapsed);
} else {
}
if (Configurations.deviceFilterTrainingEnabled) {
deviceFilter.train(device, device.payloadCharacteristic() == null);
}
return success;
}
// BluetoothStateManagerDelegate
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
final BLEDevice device = database.device(gatt.getDevice());
// Sensor characteristics
BluetoothGattService service = gatt.getService(Configurations.serviceUUID);
if (service == null) {
if (!Configurations.deviceFilterTrainingEnabled) {
// 除非是经过确认的iOS或Android设备(之前已找到传感器服务),否则请暂时忽略该设备,
// 因此请在有限的时间内忽略该设备,然后在不久的将来重试。
if (!(device.operatingSystem() == DeviceOperatingSystem.ios || device.operatingSystem() == DeviceOperatingSystem.android)) {
device.operatingSystem(DeviceOperatingSystem.ignore);
}
gatt.disconnect();
return;
} else {
}
} else {
device.invalidateCharacteristics();
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
// 确认具有signal characteristic的操作系统
if (characteristic.getUuid().equals(Configurations.androidSignalCharacteristicUUID)) {
device.operatingSystem(DeviceOperatingSystem.android);
device.signalCharacteristic(characteristic);
} else if (characteristic.getUuid().equals(Configurations.iosSignalCharacteristicUUID)) {
device.operatingSystem(DeviceOperatingSystem.ios);
device.signalCharacteristic(characteristic);
} else if (characteristic.getUuid().equals(Configurations.payloadCharacteristicUUID)) {
device.payloadCharacteristic(characteristic);
}
}
// 如果为空,则将旧有负载特征复制到负载特征
if (device.payloadCharacteristic() == null && device.legacyPayloadCharacteristic() != null) {
device.payloadCharacteristic(device.legacyPayloadCharacteristic());
}
}
if (Configurations.deviceIntrospectionEnabled) {
if (device.deviceName() == null) {
device.deviceNameCharacteristic(serviceCharacteristic(gatt, Configurations.bluetoothGenericAccessServiceUUID, Configurations.bluetoothGenericAccessServiceDeviceNameCharacteristicUUID));
if (device.supportsDeviceNameCharacteristic()) {
}
}
if (device.model() == null) {
device.modelCharacteristic(serviceCharacteristic(gatt, Configurations.bluetoothDeviceInformationServiceUUID, Configurations.bluetoothDeviceInformationServiceModelCharacteristicUUID));
if (device.supportsModelCharacteristic()) {
}
}
}
nextTask(gatt);
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
final BLEDevice device = database.
device(gatt.getDevice());
if (newState == BluetoothProfile.STATE_CONNECTED) {
device.state(DeviceState.connected);
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt.close();
device.state(DeviceState.disconnected);
if (status != 0) {
}
} else {
}
}
/// 获取Bluetooth service characteristic,如果未找到,则为null。
private BluetoothGattCharacteristic serviceCharacteristic(BluetoothGatt gatt, UUID service, UUID characteristic) {
try {
final BluetoothGattService bluetoothGattService = gatt.getService(service);
if (bluetoothGattService == null) {
return null;
}
final BluetoothGattCharacteristic bluetoothGattCharacteristic = bluetoothGattService.getCharacteristic(characteristic);
return bluetoothGattCharacteristic;
} catch (Throwable e) {
return null;
}
}
/// 给定设备的当前状态,为其建立下一个任务。
/// 这是必需的,因为所有BLE活动都是异步的,
// 因此BLEDevice对象充当存储库,用于从异步调用中整理所有设备状态和信息更新。
// 此功能检查设备状态和信息,以确定在连接设备时下一个要执行的任务(如果有)。
// 请注意,必须在每个连接上的设备上执行服务和特征发现(不能缓存),
// 因此,一旦与目标设备建立了连接,便应尽可能多地执行操作。
private NextTask nextTaskForDevice(final BLEDevice device) {
// 对于标记为.ignore的设备,没有任何任务
if (device.ignore()) {
return NextTask.nothing;
}
// 如果标记为忽略但忽略已过期,请更改为未知
if (device.operatingSystem() == DeviceOperatingSystem.ignore) {
device.operatingSystem(DeviceOperatingSystem.unknown);
}
// 对于标记为仅接收的设备,没有任务(无连接)
if (device.receiveOnly()) {
return NextTask.nothing;
}
if (Configurations.deviceIntrospectionEnabled && device.supportsModelCharacteristic() && device.model() == null) {
return NextTask.readModel;
}
if (Configurations.deviceIntrospectionEnabled && device.supportsDeviceNameCharacteristic() && device.deviceName() == null) {
return NextTask.readDeviceName;
}
// 通过读取触发特征发现的有效载荷来解析或确认操作系统,以确认操作系统
if (device.operatingSystem() == DeviceOperatingSystem.unknown ||
device.operatingSystem() == DeviceOperatingSystem.ios_tbc) {
return NextTask.readPayload;
}
// 仅在发现服务和特征并且已确认操作系统的情况下才支持立即发送
if (device.immediateSendData() != null) {
return NextTask.immediateSend;
}
// 将payload作为最高优先级
if (device.payloadData() == null) {
return NextTask.readPayload;
}
// 根据需要获取payload更新
if (device.timeIntervalSinceLastPayloadDataUpdate().value > Configurations.payloadDataUpdateTimeInterval.value) {
return NextTask.readPayload;
}
if (device.protocolIsLegacy() && device.timeIntervalSinceLastPayloadDataUpdate().value > TimeInterval.minutes(5).value) {
return NextTask.readPayload;
}
// 如果此设备无法发送数据,则写入有效负载,rssi和有效负载共享数据
if (!transmitter.isSupported()) {
// 将写有效载荷数据作为最高优先级
if (device.timeIntervalSinceLastWritePayload().value > TimeInterval.minutes(5).value) {
return NextTask.writePayload;
}
// 如果有要共享的数据,则将有效载荷共享数据写入iOS设备(在有效载荷共享和写入RSSI之间交替)
final PayloadSharingData payloadSharingData = database.payloadSharingData(device);
if (device.operatingSystem() == DeviceOperatingSystem.ios
&& payloadSharingData.data.value.length > 0
&& device.timeIntervalSinceLastWritePayloadSharing().value >= TimeInterval.seconds(15).value
&& device.timeIntervalSinceLastWritePayloadSharing().value >= device.timeIntervalSinceLastWriteRssi().value) {
return NextTask.writePayloadSharing;
}
//尽可能合理地写入RSSI(在写入RSSI和写入有效负载之间交替)
if (device.rssi() != null
&& device.timeIntervalSinceLastWriteRssi().value >= TimeInterval.seconds(15).value
&& (device.timeIntervalSinceLastWritePayload().value < Configurations.payloadDataUpdateTimeInterval.value
|| device.timeIntervalSinceLastWriteRssi().value >= device.timeIntervalSinceLastWritePayload().value)) {
return NextTask.writeRSSI;
}
//根据需要写入有效负载更新
if (device.timeIntervalSinceLastWritePayload().value > Configurations.payloadDataUpdateTimeInterval.value) {
return NextTask.writePayload;
}
}
// 将有效负载共享数据写入iOS
if (device.operatingSystem() == DeviceOperatingSystem.ios && !device.protocolIsLegacy()) {
// 如果有要共享的数据,则将有效负载共享数据写入iOS设备
final PayloadSharingData payloadSharingData = database.payloadSharingData(device);
if (device.operatingSystem() == DeviceOperatingSystem.ios
&& payloadSharingData.data.value.length > 0
&& device.timeIntervalSinceLastWritePayloadSharing().value >= TimeInterval.seconds(15).value) {
return NextTask.writePayloadSharing;
}
}
return NextTask.nothing;
}
// 给定打开的连接,为设备执行下一个任务。
// 使用此函数定义用于在设备上实现任务的实际代码(例如readPayload)。
// 任务的实际优先级在函数nextTaskForDevice()中定义。
// 有关其他设计详细信息,请参见函数nextTaskForDevice()。
private void nextTask(BluetoothGatt gatt) {
final BLEDevice device = database.device(gatt.getDevice());
final NextTask nextTask = nextTaskForDevice(device);
switch (nextTask) {
case readModel: {
final BluetoothGattCharacteristic modelCharacteristic = device.modelCharacteristic();
if (modelCharacteristic == null) {
gatt.disconnect();
return; // => onConnectionStateChange
}
if (!gatt.readCharacteristic(modelCharacteristic)) {
gatt.disconnect();
return; // => onConnectionStateChange
}
return; // => onCharacteristicRead | timeout
}
case readDeviceName: {
final BluetoothGattCharacteristic deviceNameCharacteristic = device.deviceNameCharacteristic();
if (deviceNameCharacteristic == null) {
gatt.disconnect();
return; // => onConnectionStateChange
}
if (!gatt.readCharacteristic(deviceNameCharacteristic)) {
gatt.disconnect();
return; // => onConnectionStateChange
}
return; // => onCharacteristicRead | timeout
}
case readPayload: {
final BluetoothGattCharacteristic payloadCharacteristic = device.payloadCharacteristic();
if (payloadCharacteristic == null) {
gatt.disconnect();
return; // => onConnectionStateChange
}
if (device.protocolIsLegacy()) {
gatt.requestMtu(512);
return; // => onCharacteristicRead | timeout
}
else if (!gatt.readCharacteristic(payloadCharacteristic)) {
gatt.disconnect();
return; // => onConnectionStateChange
}
// TODO incorporate Android non-auth security patch once license confirmed
return; // => onCharacteristicRead | timeout
}
case writePayload: {
final PayloadData payloadData = transmitter.payloadData();
if (payloadData == null || payloadData.value == null || payloadData.value.length == 0) {
gatt.disconnect();
return; // => onConnectionStateChange
}
final Data data = SignalCharacteristicData.encodeWritePayload(transmitter.payloadData());
writeSignalCharacteristic(gatt, NextTask.writePayload, data.value);
return;
}
case writePayloadSharing: {
final PayloadSharingData payloadSharingData = database.payloadSharingData(device);
if (payloadSharingData == null) {
gatt.disconnect();
return;
}
final Data data = SignalCharacteristicData.encodeWritePayloadSharing(payloadSharingData);
writeSignalCharacteristic(gatt, NextTask.writePayloadSharing, data.value);
return;
}
case writeRSSI: {
final BluetoothGattCharacteristic signalCharacteristic = device.signalCharacteristic();
if (signalCharacteristic == null) {
gatt.disconnect();
return;
}
final RSSI rssi = device.rssi();
if (rssi == null) {
gatt.disconnect();
return;
}
final Data data = SignalCharacteristicData.encodeWriteRssi(rssi);
writeSignalCharacteristic(gatt, NextTask.writeRSSI, data.value);
return;
}
case immediateSend: {
final BluetoothGattCharacteristic signalCharacteristic = device.signalCharacteristic();
if (signalCharacteristic == null) {
gatt.disconnect();
return;
}
final Data data = device.immediateSendData();
if (data == null) {
gatt.disconnect();
return;
}
writeSignalCharacteristic(gatt, NextTask.immediateSend, data.value);
device.immediateSendData(null);
// 确保其被发送后删除数据
return;
}
}
gatt.disconnect();
}
private void writeSignalCharacteristic(BluetoothGatt gatt, NextTask task, byte[] data) {
final BLEDevice device = database.device(gatt.getDevice());
final BluetoothGattCharacteristic signalCharacteristic = device.signalCharacteristic();
if (signalCharacteristic == null) {
gatt.disconnect();
return;
}
if (data == null || data.length == 0) {
gatt.disconnect();
return;
}
if (signalCharacteristic.getUuid().equals(Configurations.iosSignalCharacteristicUUID)) {
device.signalCharacteristicWriteValue = data;
device.signalCharacteristicWriteQueue = null;
signalCharacteristic.setValue(data);
signalCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
if (!gatt.writeCharacteristic(signalCharacteristic)) {
gatt.disconnect();
} else {
// => onCharacteristicWrite
}
return;
}
if (signalCharacteristic.getUuid().equals(Configurations.androidSignalCharacteristicUUID)) {
device.signalCharacteristicWriteValue = data;
device.signalCharacteristicWriteQueue = fragmentDataByMtu(data);
if (writeAndroidSignalCharacteristic(gatt) == WriteAndroidSignalCharacteristicResult.failed) {
gatt.disconnect();
} else {
// => onCharacteristicWrite
}
}
}
private enum WriteAndroidSignalCharacteristicResult {
moreToWrite, complete, failed
}
private WriteAndroidSignalCharacteristicResult writeAndroidSignalCharacteristic(BluetoothGatt gatt) {
final BLEDevice device = database.device(gatt.getDevice());
final BluetoothGattCharacteristic signalCharacteristic = device.signalCharacteristic();
if (signalCharacteristic == null) {
return WriteAndroidSignalCharacteristicResult.failed;
}
if (device.signalCharacteristicWriteQueue == null || device.signalCharacteristicWriteQueue.size() == 0) {
return WriteAndroidSignalCharacteristicResult.complete;
}
final byte[] data = device.signalCharacteristicWriteQueue.poll();
signalCharacteristic.setValue(data);
signalCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
if (!gatt.writeCharacteristic(signalCharacteristic)) {
return WriteAndroidSignalCharacteristicResult.failed;
} else {
return WriteAndroidSignalCharacteristicResult.moreToWrite;
}
}
/// 将数据拆分为片段,其中每个片段的长度 <= mtu
private Queue fragmentDataByMtu(byte[] data) {
final Queue fragments = new ConcurrentLinkedQueue<>();
for (int i = 0; i < data.length; i += SpecificReceiver.defaultMTU) {
final byte[] fragment = new byte[Math.min(SpecificReceiver.defaultMTU, data.length - i)];
System.arraycopy(data, i, fragment, 0, fragment.length);
fragments.add(fragment);
}
return fragments;
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
final BLEDevice device = database.device(gatt.getDevice());
final BluetoothGattCharacteristic characteristic = device.legacyPayloadCharacteristic();
if (status == BluetoothGatt.GATT_SUCCESS && characteristic != null && gatt.readCharacteristic(characteristic)) {
return; // => onCharacteristicRead | timeout
}
gatt.disconnect();
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
final BLEDevice device = database.device(gatt.getDevice());
final boolean success = (status == BluetoothGatt.GATT_SUCCESS);
if (characteristic.getUuid().equals(Configurations.payloadCharacteristicUUID)) {
final PayloadData payloadData = (characteristic.getValue() != null ? new PayloadData(characteristic.getValue()) : null);
if (success) {
if (payloadData != null) {
if(SpecificUsePayloadSupplier.checkPayloadtime(payloadData)) {
device.payloadData(payloadData);
// TODO incorporate Android non-auth security patch once license confirmed
}else{
}
// TODO incorporate Android non-auth security patch once license confirmed
} else {
}
} else {
}
}else if (characteristic.getUuid().equals(Configurations.bluetoothDeviceInformationServiceModelCharacteristicUUID)) {
final String model = characteristic.getStringValue(0);
if (success) {
if (model != null) {
device.model(model);
} else {
}
} else {
}
} else if (characteristic.getUuid().equals(Configurations.bluetoothGenericAccessServiceDeviceNameCharacteristicUUID)) {
final String deviceName = characteristic.getStringValue(0);
if (success) {
if (deviceName != null) {
device.deviceName(deviceName);
} else {
}
} else {
}
} else {
}
nextTask(gatt);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
final BLEDevice device = database.device(gatt.getDevice());
final boolean success = (status == BluetoothGatt.GATT_SUCCESS);
// Pioneer信号特性写支持
final BluetoothGattCharacteristic signalCharacteristic = device.signalCharacteristic();
if (signalCharacteristic.getUuid().equals(Configurations.androidSignalCharacteristicUUID)) {
if (success && writeAndroidSignalCharacteristic(gatt) == WriteAndroidSignalCharacteristicResult.moreToWrite) {
return;
}
}
final SignalCharacteristicDataType signalCharacteristicDataType = SignalCharacteristicData.detect(new Data(device.signalCharacteristicWriteValue));
signalCharacteristic.setValue(new byte[0]);
device.signalCharacteristicWriteValue = null;
device.signalCharacteristicWriteQueue = null;
switch (signalCharacteristicDataType) {
case payload:
if (success) {
device.registerWritePayload();
} else {
}
break;
case rssi:
if (success) {
device.registerWriteRssi();
} else {
}
break;
case payloadSharing:
if (success) {
device.registerWritePayloadSharing();
} else {
}
break;
case immediateSend:
if (success) {
device.immediateSendData(null);
} else {
// 无需重试立即发送
device.immediateSendData(null);
}
// 完成立即发送后立即关闭连接
gatt.disconnect();
// 不要执行任何其他任务
return;
default:
break;
}
nextTask(gatt);
}
// 蓝牙编码转换器
private static String bleStatus(final int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
return "GATT成功";
} else {
return "GATT失败";
}
}
private static String bleState(final int state) {
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
return "已连接";
case BluetoothProfile.STATE_DISCONNECTED:
return "已断开";
default:
return "未知状态" + state;
}
}
private static String onScanFailedErrorCodeToString(final int errorCode) {
switch (errorCode) {
case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
return "无法启动扫描,BLE已设置完毕";
case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
return "无法启动扫描,因为无法注册应用程序。";
case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
return "无法启动扫描,内部错误";
case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
return "无法启动电源优化扫描,因为不支持此功能";
default:
return "未知错误代码" + errorCode;
}
}
private static long ConvertToLong(String date, String format) {
try {
if (date != null&&format != null) {
SimpleDateFormat sf = new SimpleDateFormat(format);
return sf.parse(date).getTime();
}
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/SpecificTransmitter.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.os.ParcelUuid;
import com.ABC.pioneer.sensor.PayloadSupplier;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.BluetoothState;
import com.ABC.pioneer.sensor.datatype.Callback;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.ImmediateSendData;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadSharingData;
import com.ABC.pioneer.sensor.datatype.PayloadTimestamp;
import com.ABC.pioneer.sensor.datatype.PseudoDeviceAddress;
import com.ABC.pioneer.sensor.datatype.RSSI;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.SignalCharacteristicData;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import com.ABC.pioneer.sensor.datatype.Triple;
import com.ABC.pioneer.sensor.payload.DigitalSignature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static android.bluetooth.le.AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED;
import static android.bluetooth.le.AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE;
import static android.bluetooth.le.AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED;
import static android.bluetooth.le.AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR;
import static android.bluetooth.le.AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS;
public class SpecificTransmitter implements Transmitter, BluetoothStateManagerDelegate {
private final static long advertOffDurationMillis = TimeInterval.seconds(4).millis();
private final Context context;
private final BluetoothStateManager bluetoothStateManager;
private final PayloadSupplier payloadSupplier;
private final Database database;
private final ExecutorService operationQueue = Executors.newSingleThreadExecutor();
private final AtomicBoolean transmitterEnabled = new AtomicBoolean(false);
// 仅由startAdvert和stopExistingGattServer引用
private BluetoothGattServer bluetoothGattServer = null;
/**
* 启用蓝牙后,Transmitter会自动启动。
*/
public SpecificTransmitter(Context context, BluetoothStateManager bluetoothStateManager, Timer timer, PayloadSupplier payloadSupplier, Database database) {
this.context = context;
this.bluetoothStateManager = bluetoothStateManager;
this.payloadSupplier = payloadSupplier;
this.database = database;
bluetoothStateManager.delegates.add(this);
bluetoothStateManager(bluetoothStateManager.state());
timer.add(new AdvertLoopTask());
}
@Override
public void add(SensorDelegate delegate) {
delegates.add(delegate);
}
@Override
public void start() {
if (transmitterEnabled.compareAndSet(false, true)) {
} else {
}
}
@Override
public void stop() {
if (transmitterEnabled.compareAndSet(true, false)) {
} else {
}
}
// 广播循环
private enum AdvertLoopState {
starting, started, stopping, stopped
}
/// 获取 Bluetooth LE advertiser
private BluetoothLeAdvertiser bluetoothLeAdvertiser() {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
return null;
}
boolean supported = bluetoothAdapter.isMultipleAdvertisementSupported();
try {
final BluetoothLeAdvertiser bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser == null) {
return null;
}
return bluetoothLeAdvertiser;
} catch (Exception e) {
// 将其记录下来,因为这将使我们能够识别具有预期API实现的手机(来自Android API源代码)
return null;
}
}
private class AdvertLoopTask implements TimerDelegate {
private AdvertLoopState advertLoopState = AdvertLoopState.stopped;
private long lastStateChangeAt = System.currentTimeMillis();
private BluetoothGattServer bluetoothGattServer;
private AdvertiseCallback advertiseCallback;
private void state(final long now, AdvertLoopState state) {
final long elapsed = now - lastStateChangeAt;
this.advertLoopState = state;
lastStateChangeAt = now;
}
private long timeSincelastStateChange(final long now) {
return now - lastStateChangeAt;
}
@Override
public void bleTimer(final long now) {
if (!transmitterEnabled.get() || !isSupported() || bluetoothStateManager.state() == BluetoothState.poweredOff) {
if (advertLoopState != AdvertLoopState.stopped) {
stopAdvert(bluetoothLeAdvertiser(), advertiseCallback, bluetoothGattServer, new Callback() {
@Override
public void accept(Boolean value) {
advertiseCallback = null;
bluetoothGattServer = null;
state(now, AdvertLoopState.stopped);
}
});
}
return;
}
switch (advertLoopState) {
case stopped: {
if (bluetoothStateManager.state() == BluetoothState.poweredOn) {
final long period = timeSincelastStateChange(now);
if (period >= advertOffDurationMillis) {
final BluetoothLeAdvertiser bluetoothLeAdvertiser = bluetoothLeAdvertiser();
if (bluetoothLeAdvertiser == null) {
return;
}
state(now, AdvertLoopState.starting);
startAdvert(bluetoothLeAdvertiser, new Callback>() {
@Override
public void accept(Triple value) {
advertiseCallback = value.b;
bluetoothGattServer = value.c;
state(now, value.a ? AdvertLoopState.started : AdvertLoopState.stopped);
}
});
}
}
break;
}
case started: {
final long period = timeSincelastStateChange(now);
if (period >= Configurations.advertRefreshTimeInterval.millis()) {
final BluetoothLeAdvertiser bluetoothLeAdvertiser = bluetoothLeAdvertiser();
if (bluetoothLeAdvertiser == null) {
return;
}
state(now, AdvertLoopState.stopping);
stopAdvert(bluetoothLeAdvertiser, advertiseCallback, bluetoothGattServer, new Callback() {
@Override
public void accept(Boolean value) {
advertiseCallback = null;
bluetoothGattServer = null;
state(now, AdvertLoopState.stopped);
}
});
}
break;
}
}
}
}
// Start and stop advert
private void stopAdvert(final BluetoothLeAdvertiser bluetoothLeAdvertiser, final AdvertiseCallback advertiseCallback, final BluetoothGattServer bluetoothGattServer, final Callback callback) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
boolean result = true;
try {
if (bluetoothLeAdvertiser != null && advertiseCallback != null) {
bluetoothLeAdvertiser.stopAdvertising(advertiseCallback);
}
} catch (Throwable e) {
result = false;
}
try {
if (bluetoothGattServer != null) {
bluetoothGattServer.clearServices();
bluetoothGattServer.close();
}
} catch (Throwable e) {
result = false;
}
if (result) {
} else {
}
callback.accept(result);
}
});
}
private void startAdvert(final BluetoothLeAdvertiser bluetoothLeAdvertiser, final Callback> callback) {
operationQueue.execute(new Runnable() {
@Override
public void run() {
boolean result = true;
// 如果已经有代理参考,则停止现有广告。
// 永远不会发生这种情况,因为只有AdvertLoopTask会调用startAdvert,
// 并且仅应在先前调用stopAdvert之后再调用startAdvert。
// 记录此条件以验证是否可能发生这种情况以便后期测试数据。
if (bluetoothGattServer != null) {
try {
bluetoothGattServer.clearServices();
bluetoothGattServer.close();
} catch (Throwable e) {
}
bluetoothGattServer = null;
}
// 设定 new GATT server
try {
bluetoothGattServer = startGattServer(context, payloadSupplier, database);
} catch (Throwable e) {
result = false;
}
if (bluetoothGattServer == null) {
result = false;
} else {
try {
setGattService(context, bluetoothGattServer);
} catch (Throwable e) {
try {
bluetoothGattServer.clearServices();
bluetoothGattServer.close();
bluetoothGattServer = null;
} catch (Throwable e2) {
bluetoothGattServer = null;
}
result = false;
}
}
if (!result) {
callback.accept(new Triple(false, null, null));
return;
}
try {
final BluetoothGattServer bluetoothGattServerConfirmed = bluetoothGattServer;
final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
callback.accept(new Triple(true, this, bluetoothGattServerConfirmed));
}
@Override
public void onStartFailure(int errorCode) {
callback.accept(new Triple(false, this, bluetoothGattServerConfirmed));
}
};
startAdvertising(bluetoothLeAdvertiser, advertiseCallback);
} catch (Throwable e) {
callback.accept(new Triple(false, null, null));
}
}
});
}
@Override
public PayloadData payloadData() {
return payloadSupplier.payload(new PayloadTimestamp(new Date()), null);
}
@Override
public boolean isSupported() {
return bluetoothLeAdvertiser() != null;
}
@Override
public void bluetoothStateManager(BluetoothState didUpdateState) {
}
private void startAdvertising(final BluetoothLeAdvertiser bluetoothLeAdvertiser, final AdvertiseCallback advertiseCallback) {
final AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW)
.build();
final PseudoDeviceAddress pseudoDeviceAddress = new PseudoDeviceAddress();
final AdvertiseData data = new AdvertiseData.Builder()
.setIncludeDeviceName(false)
.setIncludeTxPowerLevel(false)
.addServiceUuid(new ParcelUuid(Configurations.serviceUUID))
.addManufacturerData(Configurations.manufacturerIdForSensor, pseudoDeviceAddress.data)
.build();
bluetoothLeAdvertiser.startAdvertising(settings, data, advertiseCallback);
}
private static BluetoothGattServer startGattServer(final Context context, final PayloadSupplier payloadSupplier, final Database database) {
final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null) {
return null;
}
// Data = rssi (4字节的int型) + payload (剩余的字节数)
final AtomicReference server = new AtomicReference<>(null);
final BluetoothGattServerCallback callback = new BluetoothGattServerCallback() {
private final Map onCharacteristicReadPayloadData = new ConcurrentHashMap<>();
private final Map onCharacteristicWriteSignalData = new ConcurrentHashMap<>();
private PayloadData onCharacteristicReadPayloadData(BluetoothDevice bluetoothDevice) {
final BLEDevice device = database.device(bluetoothDevice);
final String key = bluetoothDevice.getAddress();
if (onCharacteristicReadPayloadData.containsKey(key)) {
return onCharacteristicReadPayloadData.get(key);
}
final PayloadData payloadData = payloadSupplier.payload(new PayloadTimestamp(), device);
onCharacteristicReadPayloadData.put(key, payloadData);
return payloadData;
}
private byte[] onCharacteristicWriteSignalData(BluetoothDevice device, byte[] value) {
final String key = device.getAddress();
byte[] partialData = onCharacteristicWriteSignalData.get(key);
if (partialData == null) {
partialData = new byte[0];
}
byte[] data = new byte[partialData.length + (value == null ? 0 : value.length)];
System.arraycopy(partialData, 0, data, 0, partialData.length);
if (value != null) {
System.arraycopy(value, 0, data, partialData.length, value.length);
}
onCharacteristicWriteSignalData.put(key, data);
return data;
}
private void removeData(BluetoothDevice device) {
final String deviceAddress = device.getAddress();
for (String deviceRequestId : new ArrayList<>(onCharacteristicReadPayloadData.keySet())) {
if (deviceRequestId.startsWith(deviceAddress)) {
onCharacteristicReadPayloadData.remove(deviceRequestId);
}
}
for (String deviceRequestId : new ArrayList<>(onCharacteristicWriteSignalData.keySet())) {
if (deviceRequestId.startsWith(deviceAddress)) {
onCharacteristicWriteSignalData.remove(deviceRequestId);
}
}
}
@Override
public void onConnectionStateChange(BluetoothDevice bluetoothDevice, int status, int newState) {
final BLEDevice device = database.device(bluetoothDevice);
if (newState == BluetoothProfile.STATE_CONNECTED) {
device.state(DeviceState.connected);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
device.state(DeviceState.disconnected);
removeData(bluetoothDevice);
}
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
final BLEDevice targetDevice = database.device(device);
final TargetIdentifier targetIdentifier = targetDevice.identifier;
if (characteristic.getUuid() != Configurations.androidSignalCharacteristicUUID) {
if (responseNeeded) {
server.get().sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, offset, value);
}
return;
}
final Data data = new Data(onCharacteristicWriteSignalData(device, value));
switch (SignalCharacteristicData.detect(data)) {
case rssi: {
final RSSI rssi = SignalCharacteristicData.decodeWriteRSSI(data);
if (rssi == null) {
break;
}
// 只有receive-only Android devices写RSSI
targetDevice.operatingSystem(DeviceOperatingSystem.android);
targetDevice.receiveOnly(true);
targetDevice.rssi(rssi);
break;
}
case payload: {
final PayloadData payloadData = SignalCharacteristicData.decodeWritePayload(data);
if (payloadData == null) {
// 零碎的有效载荷数据可能不完整
break;
}
// 只有receive-only Android devices写payload
targetDevice.operatingSystem(DeviceOperatingSystem.android);
targetDevice.receiveOnly(true);
targetDevice.payloadData(payloadData);
onCharacteristicWriteSignalData.remove(device.getAddress());
break;
}
case payloadSharing: {
final PayloadSharingData payloadSharingData = SignalCharacteristicData.decodeWritePayloadSharing(data);
if (payloadSharingData == null) {
// 零碎的有效载荷数据可能不完整
break;
}
final List didSharePayloadData = payloadSupplier.payload(payloadSharingData.data);
for (SensorDelegate delegate : delegates) {
delegate.sensor(SensorType.BLE, didSharePayloadData, targetIdentifier);
}
// 只有android设备才需要写payload sharing数据
targetDevice.operatingSystem(DeviceOperatingSystem.android);
targetDevice.rssi(payloadSharingData.rssi);
for (final PayloadData payloadData : didSharePayloadData) {
final BLEDevice sharedDevice = database.device(payloadData);
sharedDevice.operatingSystem(DeviceOperatingSystem.shared);
sharedDevice.rssi(payloadSharingData.rssi);
}
break;
}
case immediateSend: {
final ImmediateSendData immediateSendData = SignalCharacteristicData.decodeImmediateSend(data);
if (immediateSendData == null) {
// 零散的立即发送数据可能不完整
break;
}
for ( SensorDelegate delegate : delegates) {
delegate.sensor(SensorType.BLE, immediateSendData, targetIdentifier);
}
break;
}
}
if (responseNeeded) {
server.get().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
final BLEDevice targetDevice = database.device(device);
if (characteristic.getUuid() == Configurations.payloadCharacteristicUUID) {
final PayloadData payloadData = onCharacteristicReadPayloadData(device);
if (offset > payloadData.value.length) {
server.get().sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset, null);
} else {
final byte[] value = Arrays.copyOfRange(payloadData.value, offset, payloadData.value.length);
server.get().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
}
} else {
server.get().sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null);
}
}
};
server.set(bluetoothManager.openGattServer(context, callback));
return server.get();
}
private static void setGattService(final Context context, final BluetoothGattServer bluetoothGattServer) {
final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null) {
return;
}
if (bluetoothGattServer == null) {
return;
}
for (BluetoothDevice device : bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)) {
bluetoothGattServer.cancelConnection(device);
}
for (BluetoothDevice device : bluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER)) {
bluetoothGattServer.cancelConnection(device);
}
bluetoothGattServer.clearServices();
// 逻辑监测 -确保现在没有Gatt服务
List services = bluetoothGattServer.getServices();
for (BluetoothGattService svc : services) {
}
final BluetoothGattService service = new BluetoothGattService(Configurations.serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
final BluetoothGattCharacteristic signalCharacteristic = new BluetoothGattCharacteristic(
Configurations.androidSignalCharacteristicUUID,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
signalCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
final BluetoothGattCharacteristic payloadCharacteristic = new BluetoothGattCharacteristic(
Configurations.payloadCharacteristicUUID,
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ);
service.addCharacteristic(signalCharacteristic);
service.addCharacteristic(payloadCharacteristic);
bluetoothGattServer.addService(service);
// 逻辑监测 - 确保只有一个Pioneer设备
services = bluetoothGattServer.getServices();
int count = 0;
for (BluetoothGattService svc : services) {
if (svc.getUuid().equals(Configurations.serviceUUID)) {
count++;
}
}
if (count > 1) {
}
}
private static String onConnectionStateChangeStatusToString(final int state) {
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
return "已连接";
case BluetoothProfile.STATE_CONNECTING:
return "连接中";
case BluetoothProfile.STATE_DISCONNECTING:
return "断开连接中";
case BluetoothProfile.STATE_DISCONNECTED:
return "已断开连接";
default:
return "未知状态" + state;
}
}
private static String onStartFailureErrorCodeToString(final int errorCode) {
switch (errorCode) {
case ADVERTISE_FAILED_DATA_TOO_LARGE:
return "无法启动广播,数据量过大";
case ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
return "无法启动广播,没有可用的广播设置。";
case ADVERTISE_FAILED_ALREADY_STARTED:
return "无法启动广播,已有广播在运行";
case ADVERTISE_FAILED_INTERNAL_ERROR:
return "无法启动广播,内部错误";
case ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
return "不支持广播功能";
default:
return "未知错误" + errorCode;
}
}
private static PayloadData add(PayloadData init_payloadData){
final long start;
final long end;
PayloadData payloadData = init_payloadData;
start = (new PayloadTimestamp()).value.getTime();
end = start + 60*1000*6;
java.text.SimpleDateFormat dateformat=new java.text.SimpleDateFormat("yyyy:MM:dd HH-mm-ss");
dateformat.setTimeZone(TimeZone.getTimeZone("GMT+08"));
String date_str0=dateformat.format(start);
String date_str1=dateformat.format(end);
Data term_of_validity = new Data();
term_of_validity.append(date_str0);
term_of_validity.append(date_str1);
payloadData.append(term_of_validity);
DigitalSignature assign = new DigitalSignature();
payloadData.append(assign.genMAC());
return payloadData;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/Timer.java
================================================
package com.ABC.pioneer.sensor.ble;
import android.content.Context;
import android.os.PowerManager;
import com.ABC.pioneer.sensor.analysis.Sample;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
// 稳定的一秒钟计时器,用于控制BLE操作。拥有可靠的计时器来启动和停止扫描对于可靠的检测和跟踪至关重要。
// 经过测试可能会出现失败,特定情况不具体列出,详见Pioneer文档
public class Timer {
private final Sample sample = new Sample();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private final PowerManager.WakeLock wakeLock;
private final AtomicLong now = new AtomicLong(0);
private final Queue delegates = new ConcurrentLinkedQueue<>();
private final Runnable runnable = new Runnable() {
@Override
public void run() {
for (TimerDelegate delegate : delegates) {
try {
delegate.bleTimer(now.get());
} catch (Throwable e) {
}
}
}
};
public Timer(Context context) {
final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Sensor:BLETimer");
wakeLock.acquire();
final Thread timerThread = new Thread(new Runnable() {
private long last = 0;
@Override
public void run() {
while (true) {
now.set(System.currentTimeMillis());
final long elapsed = now.get() - last;
if (elapsed >= 1000) {
if (last != 0) {
sample.add(elapsed);
executorService.execute(runnable);
}
last = now.get();
}
try {
Thread.sleep(500);
} catch (Throwable e) {
}
}
}
});
timerThread.setPriority(Thread.MAX_PRIORITY);
timerThread.setName("Sensor.BLETimer");
timerThread.start();
}
@Override
protected void finalize() {
wakeLock.release();
}
/// 添加时间通知委托
public void add(TimerDelegate delegate) {
delegates.add(delegate);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/TimerDelegate.java
================================================
package com.ABC.pioneer.sensor.ble;
public interface TimerDelegate {
void bleTimer(long currentTimeMillis);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/Transmitter.java
================================================
package com.ABC.pioneer.sensor.ble;
import com.ABC.pioneer.sensor.Sensor;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
// Beacon transmitter广播固定服务UUID,以启用iOS的后台扫描。
// 当iOS进入后台模式时,UUID将从广播中消失
// 因此Android设备需要搜索Apple设备,然后连接并发现服务以读取UUID。
public interface Transmitter extends Sensor {
// 接收Beacon检测事件的delegate。
// 这是必要的,因为某些Android设备(三星J6)不支持BLE传输
// 因此要使Beacon Characteristic可写的话,此类设备提供了一种机制,
// 可通过发送其自身的beacon code和RSSI作为数据来检测Beacon transmitter并知道其自身的存在到transmitter。
Queue delegates = new ConcurrentLinkedQueue<>();
// 获取当前Payload。
PayloadData payloadData();
//是否支持Transmitter功能
// 如果BLE广播功能正常,则返回True
boolean isSupported();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/TxPower.java
================================================
package com.ABC.pioneer.sensor.ble;
public class TxPower {
public final int value;
public TxPower(int value) {
this.value = value;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEAppleManuSeg.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import com.ABC.pioneer.sensor.datatype.Data;
public class BLEAppleManuSeg {
public final int type;
public final int reportedLength;
public final byte[] data;
// 根据网络顺序,在此处我们采取大端的方式存储
public final Data raw;
public BLEAppleManuSeg(int type, int reportedLength, byte[] dataBigEndian, Data raw) {
this.type = type;
this.reportedLength = reportedLength;
this.data = dataBigEndian;
this.raw = raw;
}
@Override
public String toString() {
return raw.hexEncodedString();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEDeviceFilter.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import android.bluetooth.le.ScanRecord;
import android.content.Context;
import com.ABC.pioneer.sensor.ble.BLEDevice;
import com.ABC.pioneer.sensor.ble.Configurations;
import com.ABC.pioneer.sensor.data.TextFile;
import com.ABC.pioneer.sensor.datatype.Data;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// 设备筛选器,用于避免连接到绝对不能承载sensor服务的设备。
public class BLEDeviceFilter {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final List filterPatterns;
private final TextFile textFile;
private final Map samples = new HashMap<>();
// 训练samples的计数
private final static class ShouldIgnore {
public long yes = 0;
public long no = 0;
}
// 基于消息内容过滤设备的pattern
public final static class FilterPattern {
public final String regularExpression;
public final Pattern pattern;
public FilterPattern(final String regularExpression, final Pattern pattern) {
this.regularExpression = regularExpression;
this.pattern = pattern;
}
}
// 匹配过滤器pattern
public final static class MatchingPattern {
public final FilterPattern filterPattern;
public final String message;
public MatchingPattern(FilterPattern filterPattern, String message) {
this.filterPattern = filterPattern;
this.message = message;
}
}
/// BLE设备筛选器,用于将设备与BLESensorConfiguration.deviceFilterFeaturePatterns中定义的筛选器进行匹配。
public BLEDeviceFilter() {
this(null, null, Configurations.deviceFilterFeaturePatterns);
}
// BLE设备筛选器,用于将设备与BLESensorConfiguration.deviceFilterFeaturePatterns进行匹配
// 并将广播数据写入文件进行分析。
public BLEDeviceFilter(final Context context, final String file) {
this(context, file, Configurations.deviceFilterFeaturePatterns);
}
/// BLE设备过滤器,用于根据给定的patterns匹配设备
// 并将广播数据写入文件进行分析。
public BLEDeviceFilter(final Context context, final String file, final String[] patterns) {
if (context == null || file == null) {
textFile = null;
} else {
textFile = new TextFile(context, file);
if (textFile.empty()) {
textFile.write("time,ignore,featureData,scanRecordRawData,identifier,rssi,deviceModel,deviceName");
}
}
if (Configurations.deviceFilterTrainingEnabled || patterns == null || patterns.length == 0) {
filterPatterns = null;
} else {
filterPatterns = compilePatterns(patterns);
}
}
// Pattern匹配函数
// 在特征数据的十六进制表示形式上使用正则表达式可最大程度地提高灵活性和可用性
/// 按顺序将消息与所有pattern匹配,返回匹配pattern,或返回为null
protected static FilterPattern match(final List filterPatterns, final String message) {
if (message == null) {
return null;
}
for (final FilterPattern filterPattern : filterPatterns) {
try {
final Matcher matcher = filterPattern.pattern.matcher(message);
if (matcher.find()) {
return filterPattern;
}
} catch (Throwable e) {
}
}
return null;
}
/// 将正则表达式编译为patterns.
protected static List compilePatterns(final String[] regularExpressions) {
final List filterPatterns = new ArrayList<>(regularExpressions.length);
for (final String regularExpression : regularExpressions) {
try {
final Pattern pattern = Pattern.compile(regularExpression, Pattern.CASE_INSENSITIVE);
if (regularExpression != null && !regularExpression.isEmpty() && pattern != null) {
final FilterPattern filterPattern = new FilterPattern(regularExpression, pattern);
filterPatterns.add(filterPattern);
}
} catch (Throwable e) {
}
}
return filterPatterns;
}
/// 从特定于制造商的数据中提取消息
protected final static List extractMessages(final byte[] rawScanRecordData) {
// 解析扫描响应数据中的raw扫描记录数据
if (rawScanRecordData == null || rawScanRecordData.length == 0) {
return null;
}
final BLEScanResponseData bleScanResponseData = BLEParser.parseScanResponse(rawScanRecordData, 0);
// 将扫描响应数据解析为特定于制造商的数据
if (bleScanResponseData == null || bleScanResponseData.segments == null || bleScanResponseData.segments.isEmpty()) {
return null;
}
final List bleManuDataList = BLEParser.extractManufacturerData(bleScanResponseData.segments);
// 将制造商特定的数据解析为消息
if (bleManuDataList == null || bleManuDataList.isEmpty()) {
return null;
}
final List bleAppleManuSegs = BLEParser.extractAppleManufacturerSegments(bleManuDataList);
// 将数据段转换为消息
if (bleAppleManuSegs == null || bleAppleManuSegs.isEmpty()) {
return null;
}
final List messages = new ArrayList<>(bleAppleManuSegs.size());
for (BLEAppleManuSeg segment : bleAppleManuSegs) {
if (segment != null && segment.raw != null && segment.raw.value.length > 0) {
messages.add(segment.raw);
}
}
return messages;
}
// Filtering函数
/// 从扫描记录中提取特征数据
private List extractFeatures(final ScanRecord scanRecord) {
if (scanRecord == null) {
return null;
}
// 获取相应数据
final List featureList = new ArrayList<>();
final List messages = extractMessages(scanRecord.getBytes());
if (messages != null) {
featureList.addAll(messages);
}
return featureList;
}
/// 将训练示例添加到自适应滤波器。
public synchronized void train(final BLEDevice device, final boolean ignore) {
final ScanRecord scanRecord = device.scanRecord();
// 从扫描记录中获取特征数据
if (scanRecord == null) {
return;
}
final Data scanRecordData = (scanRecord.getBytes() == null ? null : new Data(scanRecord.getBytes()));
if (scanRecordData == null) {
return;
}
final List featureList = extractFeatures(scanRecord);
if (featureList == null) {
return;
}
// 更新忽略要素数据的是/否计数
for (Data featureData : featureList) {
ShouldIgnore shouldIgnore = samples.get(featureData);
if (shouldIgnore == null) {
shouldIgnore = new ShouldIgnore();
samples.put(featureData, shouldIgnore);
}
if (ignore) {
shouldIgnore.yes++;
} else {
shouldIgnore.no++;
}
// 将样本写入文本文件进行分析
if (textFile == null) {
return;
}
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append('"');
stringBuilder.append(dateFormatter.format(new Date()));
stringBuilder.append('"');
stringBuilder.append(',');
stringBuilder.append(ignore ? 'Y' : 'N');
stringBuilder.append(',');
stringBuilder.append(featureData.hexEncodedString());
stringBuilder.append(',');
stringBuilder.append(scanRecordData.hexEncodedString());
stringBuilder.append(',');
stringBuilder.append(device.identifier.value);
stringBuilder.append(',');
if (device.rssi() != null) {
stringBuilder.append(device.rssi().value);
}
stringBuilder.append(',');
if (device.model() != null) {
stringBuilder.append('"');
stringBuilder.append(device.model());
stringBuilder.append('"');
}
stringBuilder.append(',');
if (device.deviceName() != null) {
stringBuilder.append('"');
stringBuilder.append(device.deviceName());
stringBuilder.append('"');
}
textFile.write(stringBuilder.toString());
}
}
/// 将过滤器模式与数据项进行匹配,并返回第一个匹配项
protected final static MatchingPattern match(final List patternList, final Data rawData) {
// 没有匹配到的pattern
if (patternList == null || patternList.isEmpty()) {
return null;
}
// 如果raw data为空
if (rawData == null || rawData.value == null || rawData.value.length == 0) {
return null;
}
// 提取信息
final List messages = extractMessages(rawData.value);
if (messages == null || messages.isEmpty()) {
return null;
}
for (Data message : messages) {
if (message == null) {
continue;
}
try {
final String hexEncodedString = message.hexEncodedString();
final FilterPattern pattern = match(patternList, hexEncodedString);
if (pattern != null) {
return new MatchingPattern(pattern, hexEncodedString);
}
} catch (Throwable e) {
}
}
return null;
}
/// 将扫描记录消息与所有已注册模式匹配,返回匹配模式或为空。
public MatchingPattern match(final BLEDevice device) {
try {
final ScanRecord scanRecord = device.scanRecord();
// 没有任何扫描记录数据就无法匹配设备
if (scanRecord == null) {
return null;
}
// 无法匹配数据为空的扫描记录
final byte[] bytes = scanRecord.getBytes();
if (bytes == null) {
return null;
}
final Data rawData = new Data(bytes);
// 尝试配对
final MatchingPattern matchingPattern = match(filterPatterns, rawData);
if (matchingPattern == null || matchingPattern.filterPattern == null || matchingPattern.filterPattern.pattern == null || matchingPattern.filterPattern.regularExpression == null || matchingPattern.message == null) {
return null;
} else {
return matchingPattern;
}
} catch (Throwable e) {
return null;
}
}
/// 是否应该根据扫描记录数据忽略设备?
private boolean ignoreBasedOnStatistics(final BLEDevice device) {
final ScanRecord scanRecord = device.scanRecord();
// 不要忽略没有任何扫描记录数据的设备信息
if (scanRecord == null) {
return false;
}
// 从扫描记录中提取特征数据
// 不要忽略没有任何特征数据的设备信息
final List featureList = extractFeatures(scanRecord);
if (featureList == null) {
return false;
}
for (Data featureData : featureList) {
// 获取training example统计信息
final ShouldIgnore shouldIgnore = samples.get(featureData);
// 不要基于未知特征数据忽略设备
if (shouldIgnore == null) {
return false;
}
// 即使仅仅有一个符合的例子,也不忽略设备信息
if (shouldIgnore.no > 0) {
return false;
}
// 如果签名已注册忽略两次以上,则忽略设备信息
if (shouldIgnore.yes > 2) {
return true;
}
}
// 即使以上的规则都不符合,也不忽略设备信息
return false;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEManuData.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import com.ABC.pioneer.sensor.datatype.Data;
public class BLEManuData {
public final int manufacturer;
public final byte[] data; // BIG ENDIAN (network order) AT THIS POINT
public final Data raw;
// 根据网络顺序,在此处我们采取大端的方式存储
public BLEManuData(final int manufacturer, final byte[] dataBigEndian, final Data raw) {
this.manufacturer = manufacturer;
this.data = dataBigEndian;
this.raw = raw;
}
@Override
public String toString() {
return "BLEAdvertManufacturerData{" +
"manufacturer=" + manufacturer +
", data=" + new Data(data).hexEncodedString() +
", raw=" + raw.hexEncodedString() +
'}';
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEParser.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.UInt8;
import java.util.ArrayList;
import java.util.List;
public class BLEParser {
public static BLEScanResponseData parseScanResponse(byte[] raw, int offset) {
// 将其分隔成多个数据段,直至二进制数据段结束
return new BLEScanResponseData(raw.length - offset, extractSegments(raw, offset));
}
public static List extractSegments(byte[] raw, int offset) {
int position = offset;
ArrayList segments = new ArrayList();
int segmentLength;
int segmentType;
byte[] segmentData;
Data rawData;
int c;
while (position < raw.length) {
if ((position + 2) <= raw.length) {
segmentLength = (byte)raw[position++] & 0xff;
segmentType = (byte)raw[position++] & 0xff;
// Note: 不知道的数据类型将记作 “unknown”
// 检查报告后的长度与实际剩余数据长度
if ((position + segmentLength - 1) <= raw.length) {
segmentData = subDataBigEndian(raw, position, segmentLength - 1); // Note: type IS INCLUDED in length
rawData = new Data(subDataBigEndian(raw, position - 2, segmentLength + 1));
position += segmentLength - 1;
segments.add(new BLESeg(BLESegType.typeFor(segmentType), segmentLength - 1, segmentData, rawData));
} else {
// 数据的长度出现错误,直接跳转到数据结尾
position = raw.length;
}
} else {
// 无效的数据段,直接跳转到数据结尾
position = raw.length;
}
}
return segments;
}
// 十六进制
public static String hex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
// 二进制转化为字符串
public static String binaryString(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'));
result.append(" ");
}
return result.toString();
}
// 大端处理subData
public static byte[] subDataBigEndian(byte[] raw, int offset, int length) {
if (raw == null) {
return new byte[]{};
}
if (offset < 0 || length <= 0) {
return new byte[]{};
}
if (length + offset > raw.length) {
return new byte[]{};
}
byte[] data = new byte[length];
int position = offset;
for (int c = 0;c < length;c++) {
data[c] = raw[position++];
}
return data;
}
// 小端处理subData
public static byte[] subDataLittleEndian(byte[] raw, int offset, int length) {
if (raw == null) {
return new byte[]{};
}
if (offset < 0 || length <= 0) {
return new byte[]{};
}
if (length + offset > raw.length) {
return new byte[]{};
}
byte[] data = new byte[length];
int position = offset + length - 1;
for (int c = 0;c < length;c++) {
data[c] = raw[position--];
}
return data;
}
// 获取发射功率
public static Integer extractTxPower(List segments) {
// 在列表中找到txPower代码段
for (BLESeg segment : segments) {
if (segment.type == BLESegType.txPowerLevel) {
return (new UInt8((int)segment.data[0])).value;
}
}
return null;
}
public static List extractManufacturerData(List segments) {
// 在列表中找到制造商数据代码段
List manufacturerData = new ArrayList<>();
for (BLESeg segment : segments) {
if (segment.type == BLESegType.manufacturerData) {
// 确保数据有足够的数据长度
if (segment.data.length < 2) {
continue;
// 可能会产生和制造商数据相同的数据类型的有效段
}
// 创建制造商数据段
int intValue = ((segment.data[1]&0xff) << 8) | (segment.data[0]&0xff);
manufacturerData.add(new BLEManuData(intValue,subDataBigEndian(segment.data,2,segment.dataLength - 2), segment.raw));
}
}
return manufacturerData;
}
public static List extractAppleManufacturerSegments(List manuData) {
final List appleSegments = new ArrayList<>();
for (BLEManuData manu : manuData) {
int bytePos = 0;
while (bytePos < manu.data.length) {
final byte type = manu.data[bytePos];
final int typeValue = type & 0xFF;
// "01" 意味着legacy service UUID编码的时候没有长度数据
if (type == 0x01) {
final int length = manu.data.length - bytePos - 1;
final Data data = new Data(subDataBigEndian(manu.data, bytePos + 1, length));
final Data raw = new Data(subDataBigEndian(manu.data, bytePos, manu.data.length - bytePos));
final BLEAppleManuSeg segment = new BLEAppleManuSeg(typeValue, length, data.value, raw);
appleSegments.add(segment);
bytePos = manu.data.length;
}
// 根据类型长度数据进行解析
else {
final int length = manu.data[bytePos + 1] & 0xFF;
final int maxLength = (length < manu.data.length - bytePos - 2 ? length : manu.data.length - bytePos - 2);
final Data data = new Data(subDataBigEndian(manu.data, bytePos + 2, maxLength));
final Data raw = new Data(subDataBigEndian(manu.data, bytePos, maxLength + 2));
final BLEAppleManuSeg segment = new BLEAppleManuSeg(typeValue, length, data.value, raw);
appleSegments.add(segment);
bytePos += (maxLength + 2);
}
}
}
return appleSegments;
}
public static List extractServiceUUID16Data(List segments) {
// 在列表中找到serviceData代码段
List serviceData = new ArrayList<>();
for (BLESeg segment : segments) {
if (segment.type == BLESegType.serviceUUID16Data) {
// 确保数据有足够的数据长度
if (segment.data.length < 2) {
continue;
//可能会产生和制造商数据相同的数据类型的有效段
}
// 创建服务数据段
final byte[] serviceUUID16LittleEndian = subDataLittleEndian(segment.data,0,2);
serviceData.add(new BLEServiceData(serviceUUID16LittleEndian, subDataBigEndian(segment.data,2,segment.dataLength - 2), segment.raw));
}
}
return serviceData;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEScanResponseData.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import java.util.List;
public class BLEScanResponseData {
public int dataLength;
public List segments;
public BLEScanResponseData(int dataLength, List segments) {
this.dataLength = dataLength;
this.segments = segments;
}
@Override
public String toString() {
return segments.toString();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLESeg.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import com.ABC.pioneer.sensor.datatype.Data;
public class BLESeg {
public final BLESegType type;
public final int dataLength;
public final byte[] data;
// 根据网络顺序,在此处我们采取大端的方式存储
public final Data raw;
public BLESeg(BLESegType type, int dataLength, byte[] data, Data raw) {
this.type = type;
this.dataLength = dataLength;
this.data = data;
this.raw = raw;
}
@Override
public String toString() {
return "BLEAdvertSegment{" +
"type=" + type +
", dataLength=" + dataLength +
", data=" + new Data(data).hexEncodedString() +
", raw=" + raw.hexEncodedString() +
'}';
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLESegType.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import java.util.HashMap;
import java.util.Map;
/// BLE广告类型-注意:出于某些原因,我们仅列出在Pioneer中使用的广告类型
public enum BLESegType {
unknown("unknown", 0x00),
serviceUUID16IncompleteList("serviceUUID16IncompleteList", 0x02),
serviceUUID16CompleteList("serviceUUID16CompleteList", 0x03),
serviceUUID32IncompleteList("serviceUUID32IncompleteList", 0x04),
serviceUUID32CompleteList("serviceUUID32CompleteList", 0x05),
serviceUUID128IncompleteList("serviceUUID128IncompleteList", 0x06),
serviceUUID128CompleteList("serviceUUID128CompleteList", 0x07),
deviceNameShortened("deviceNameShortened", 0x08),
deviceNameComplete("deviceNameComplete", 0x09),
txPowerLevel("txPower",0x0A),
deviceClass("deviceClass",0x0D),
simplePairingHash("simplePairingHash",0x0E),
simplePairingRandomiser("simplePairingRandomiser",0x0F),
deviceID("deviceID",0x10),
serviceUUID16Data("serviceUUID16Data", 0x16),
meshMessage("meshMessage",0x2A),
meshBeacon("meshBeacon",0x2B),
bigInfo("bigInfo",0x2C),
broadcastCode("broadcastCode",0x2D),
manufacturerData("manufacturerData", 0xFF)
;
private static final Map BY_LABEL = new HashMap<>();
private static final Map BY_CODE = new HashMap<>();
static {
for (BLESegType e : values()) {
BY_LABEL.put(e.label, e);
BY_CODE.put(e.code, e);
}
}
public final String label;
public final int code;
private BLESegType(String label, int code) {
this.label = label;
this.code = code;
}
public static BLESegType typeFor(int code) {
BLESegType type = BY_CODE.get(code);
if (null == type) {
return BY_LABEL.get("unknown");
}
return type;
}
public static BLESegType typeFor(String commonName) {
BLESegType type = BY_LABEL.get(commonName);
if (null == type) {
return BY_LABEL.get("unknown");
}
return type;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/ble/filter/BLEServiceData.java
================================================
package com.ABC.pioneer.sensor.ble.filter;
import com.ABC.pioneer.sensor.datatype.Data;
public class BLEServiceData {
public final byte[] service;
public final byte[] data;
// 根据网络顺序,在此处我们采取大端的方式存储
public final Data raw;
public BLEServiceData(final byte[] service, final byte[] dataBigEndian, final Data raw) {
this.service = service;
this.data = dataBigEndian;
this.raw = raw;
}
@Override
public String toString() {
return "BLEAdvertServiceData{" +
"service=" + new Data(service).hexEncodedString() +
", data=" + new Data(data).hexEncodedString() +
", raw=" + raw.hexEncodedString() +
'}';
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/client/controller/PioneerClient.java
================================================
package com.ABC.pioneer.sensor.client.controller;
import android.content.Context;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
// HTTPS的Client实现
public class PioneerClient {
static int item;
private String host ;
private int port;
private Context context;
public PioneerClient(){
}
public PioneerClient(String host, int port, Context context){
this.host = host;
this.port = port;
this.context = context;
}
// 创建并初始化SSLContext
public SSLContext createSSLContext(){
try{
KeyStore keyStore = KeyStore.getInstance("PKCS12");
InputStream certificates = this.context.getAssets().open("server.p12");
keyStore.load(certificates,"010320".toCharArray());
// 创建密钥管理器
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "010320".toCharArray());
KeyManager[] km = keyManagerFactory.getKeyManagers();
// 创建信任管理器
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] tm = trustManagerFactory.getTrustManagers();
// 初始化SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(km, tm, null);
return sslContext;
} catch (Exception ex){
ex.printStackTrace();
}
return null;
}
// 开始运行服务器
public SSLSocket run() throws IOException{
//SSLContext
SSLContext sslContext = this.createSSLContext();
// Create socket factory
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
//创建Socket类型的对象,并提供服务器的主机名和端口号
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(this.host, this.port);
return socket;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/BatteryLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.text.SimpleDateFormat;
import java.util.Date;
// Andorid, CSV电池日志 ,用于事后分析和可视化
public class BatteryLog {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final static TimeInterval updateInterval = TimeInterval.seconds(30);
private final Context context;
private final TextFile textFile;
// battery
public BatteryLog(final Context context, final String filename) {
this.context = context;
textFile = new TextFile(context, filename);
if (textFile.empty()) {
textFile.write("time,source,level");
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
update();
} catch (Throwable e) {
}
try {
Thread.sleep(updateInterval.millis());
} catch (Throwable e) {
}
}
}
}).start();
}
// update
private void update() {
final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
final Intent batteryStatus = context.registerReceiver(null, intentFilter);
if (batteryStatus == null) {
return;
}
final int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
final boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
final int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
final int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
final float batteryLevel = level * 100 / (float) scale;
final String powerSource = (isCharging ? "external" : "battery");
final String timestamp = dateFormatter.format(new Date());
textFile.write(timestamp + "," + powerSource + "," + batteryLevel);
System.out.println(timestamp + "," + powerSource + "," + batteryLevel);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/CalibrationLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import com.ABC.pioneer.sensor.DefaultSensorDelegate;
import java.text.SimpleDateFormat;
import java.util.Date;
// 用于事后分析和可视化的CSV联系日志
public class CalibrationLog extends DefaultSensorDelegate {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final TextFile textFile;
public CalibrationLog(final Context context, final String filename) {
textFile = new TextFile(context, filename);
if (textFile.empty()) {
textFile.write("time,payload,rssi,x,y,z");
}
}
private static String timestamp() {
return dateFormatter.format(new Date());
}
private static String csv(String value) {
return TextFile.csv(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/ConcretePayloadDataFormatter.java
================================================
package com.ABC.pioneer.sensor.data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
public class ConcretePayloadDataFormatter implements PayloadDataFormatter {
@Override
public String shortFormat(PayloadData payloadData) {
return payloadData.shortName();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/ContactLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import com.ABC.pioneer.sensor.DefaultSensorDelegate;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/// CSV联系人日志,用于事后分析和可视化
public class ContactLog extends DefaultSensorDelegate {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final TextFile textFile;
private final PayloadDataFormatter payloadDataFormatter;
public ContactLog(final Context context, final String filename, PayloadDataFormatter payloadDataFormatter) {
textFile = new TextFile(context, filename);
this.payloadDataFormatter = payloadDataFormatter;
if (textFile.empty()) {
textFile.write("time,sensor,id,detect,read,measure,share,visit,data");
}
}
public ContactLog(final Context context, final String filename) {
this(context, filename, new ConcretePayloadDataFormatter());
}
private String timestamp() {
return dateFormatter.format(new Date());
}
private String csv(String value) {
return TextFile.csv(value);
}
// MARK:- SensorDelegate
@Override
public void sensor(SensorType sensor, TargetIdentifier didDetect) {
textFile.write(timestamp() + "," + sensor.name() + "," + csv(didDetect.value) + ",1,,,,,");
}
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
textFile.write(timestamp() + "," + sensor.name() + "," + csv(fromTarget.value) + ",,2,,,," + csv(payloadDataFormatter.shortFormat(didRead)));
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
final String prefix = timestamp() + "," + sensor.name() + "," + csv(fromTarget.value);
for (PayloadData payloadData : didShare) {
textFile.write(prefix + ",,,,4,," + csv(payloadDataFormatter.shortFormat(payloadData)));
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/DetectionLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import com.ABC.pioneer.sensor.DefaultSensorDelegate;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/// CSV联系人日志,用于事后分析和可视化
public class DetectionLog extends DefaultSensorDelegate {
private final TextFile textFile;
private final PayloadData payloadData;
private final String deviceName = android.os.Build.MODEL;
private final String deviceOS = Integer.toString(android.os.Build.VERSION.SDK_INT);
private final Map payloads = new ConcurrentHashMap<>();
private final PayloadDataFormatter payloadDataFormatter;
public DetectionLog(final Context context, final String filename, final PayloadData payloadData, PayloadDataFormatter payloadDataFormatter) {
textFile = new TextFile(context, filename);
this.payloadData = payloadData;
this.payloadDataFormatter = payloadDataFormatter;
write();
}
public DetectionLog(final Context context, final String filename, final PayloadData payloadData) {
this(context, filename, payloadData, new ConcretePayloadDataFormatter());
}
private String csv(String value) {
return TextFile.csv(value);
}
private void write() {
final StringBuilder content = new StringBuilder();
content.append(csv(deviceName));
content.append(',');
content.append("Android");
content.append(',');
content.append(csv(deviceOS));
content.append(',');
content.append(csv(payloadDataFormatter.shortFormat(payloadData)));
final List payloadList = new ArrayList<>(payloads.size());
for (String payload : payloads.keySet()) {
if (payload.equals(payloadDataFormatter.shortFormat(payloadData))) {
continue;
}
payloadList.add(payload);
}
Collections.sort(payloadList);
for (String payload : payloadList) {
content.append(',');
content.append(payload);
}
content.append("\n");
textFile.overwrite(content.toString());
}
// MARK:- SensorDelegate
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
if (payloads.put(payloadDataFormatter.shortFormat(didRead), fromTarget.value) == null) {
write();
}
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
for (PayloadData payloadData : didShare) {
if (payloads.put(payloadDataFormatter.shortFormat(payloadData), fromTarget.value) == null) {
write();
}
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/EventTimeIntervalLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import com.ABC.pioneer.sensor.DefaultSensorDelegate;
import com.ABC.pioneer.sensor.analysis.Sample;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// Android 事件时间间隔的CSV日志,用于事件后分析和可视化
public class EventTimeIntervalLog extends DefaultSensorDelegate {
private final TextFile textFile;
private final PayloadData payloadData;
private final EventType eventType;
private final Map targetIdentifierToPayload = new ConcurrentHashMap<>();
private final Map payloadToTime = new ConcurrentHashMap<>();
private final Map payloadToSample = new ConcurrentHashMap<>();
public enum EventType {
detect,read,measure,share,sharedPeer,visit
}
public EventTimeIntervalLog(final Context context, final String filename, final PayloadData payloadData, final EventType eventType) {
this.textFile = new TextFile(context, filename);
this.payloadData = payloadData;
this.eventType = eventType;
}
private String csv(String value) {
return TextFile.csv(value);
}
private void add(String payload) {
final Date time = payloadToTime.get(payload);
final Sample sample = payloadToSample.get(payload);
if (time == null || sample == null) {
payloadToTime.put(payload, new Date());
payloadToSample.put(payload, new Sample());
return;
}
final Date now = new Date();
payloadToTime.put(payload, now);
sample.add((now.getTime() - time.getTime()) / 1000d);
write();
}
// MARK:- SensorDelegate
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
final String payload = didRead.shortName();
targetIdentifierToPayload.put(fromTarget, payload);
if (eventType == EventType.read) {
add(payload);
}
}
@Override
public void sensor(SensorType sensor, TargetIdentifier didDetect) {
if (eventType == EventType.detect) {
final String payload = targetIdentifierToPayload.get(didDetect);
if (payload == null) {
return;
}
add(payload);
}
}
@Override
public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) {
if (eventType == EventType.measure) {
final String payload = targetIdentifierToPayload.get(fromTarget);
if (payload == null) {
return;
}
add(payload);
}
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
if (eventType == EventType.share) {
final String payload = targetIdentifierToPayload.get(fromTarget);
if (payload == null) {
return;
}
add(payload);
} else if (eventType == EventType.sharedPeer) {
for (final PayloadData sharedPeer : didShare) {
final String payload = sharedPeer.shortName();
if (payload == null) {
return;
}
add(payload);
}
}
}
private void write() {
final StringBuilder content = new StringBuilder("event,central,peripheral,count,mean,sd,min,max\n");
final List payloadList = new ArrayList<>();
final String event = csv(eventType.name());
final String centralPayload = csv(payloadData.shortName());
for (String payload : payloadToSample.keySet()) {
if (payload.equals(payloadData.shortName())) {
continue;
}
payloadList.add(payload);
}
Collections.sort(payloadList);
for (String payload : payloadList) {
final Sample sample = payloadToSample.get(payload);
if (sample == null) {
continue;
}
if (sample.mean() == null || sample.standardDeviation() == null || sample.min() == null || sample.max() == null) {
continue;
}
content.append(event);
content.append(',');
content.append(centralPayload);
content.append(',');
content.append(csv(payload));
content.append(',');
content.append(sample.count());
content.append(',');
content.append(sample.mean());
content.append(',');
content.append(sample.standardDeviation());
content.append(',');
content.append(sample.min());
content.append(',');
content.append(sample.max());
content.append('\n');
}
textFile.overwrite(content.toString());
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/PayloadDataFormatter.java
================================================
package com.ABC.pioneer.sensor.data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
public interface PayloadDataFormatter {
String shortFormat(PayloadData payloadData);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/StatisticsLog.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import com.ABC.pioneer.sensor.DefaultSensorDelegate;
import com.ABC.pioneer.sensor.analysis.Sample;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.Proximity;
import com.ABC.pioneer.sensor.datatype.SensorType;
import com.ABC.pioneer.sensor.datatype.TargetIdentifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// CSV联系人日志,用于事后分析和可视化
public class StatisticsLog extends DefaultSensorDelegate {
private final TextFile textFile;
private final PayloadData payloadData;
private final Map identifierToPayload = new ConcurrentHashMap<>();
private final Map payloadToTime = new ConcurrentHashMap<>();
private final Map payloadToSample = new ConcurrentHashMap<>();
public StatisticsLog(final Context context, final String filename, final PayloadData payloadData) {
textFile = new TextFile(context, filename);
this.payloadData = payloadData;
}
private String csv(String value) {
return TextFile.csv(value);
}
private void add(TargetIdentifier identifier) {
final String payload = identifierToPayload.get(identifier);
if (payload == null) {
return;
}
add(payload);
}
private void add(String payload) {
final Date time = payloadToTime.get(payload);
final Sample sample = payloadToSample.get(payload);
if (time == null || sample == null) {
payloadToTime.put(payload, new Date());
payloadToSample.put(payload, new Sample());
return;
}
final Date now = new Date();
payloadToTime.put(payload, now);
sample.add((now.getTime() - time.getTime()) / 1000d);
write();
}
private void write() {
final StringBuilder content = new StringBuilder("payload,count,mean,sd,min,max\n");
final List payloadList = new ArrayList<>();
for (String payload : payloadToSample.keySet()) {
if (payload.equals(payloadData.shortName())) {
continue;
}
payloadList.add(payload);
}
Collections.sort(payloadList);
for (String payload : payloadList) {
final Sample sample = payloadToSample.get(payload);
if (sample == null) {
continue;
}
if (sample.mean() == null || sample.standardDeviation() == null || sample.min() == null || sample.max() == null) {
continue;
}
content.append(csv(payload));
content.append(',');
content.append(sample.count());
content.append(',');
content.append(sample.mean());
content.append(',');
content.append(sample.standardDeviation());
content.append(',');
content.append(sample.min());
content.append(',');
content.append(sample.max());
content.append('\n');
}
textFile.overwrite(content.toString());
}
// MARK:- SensorDelegate
@Override
public void sensor(SensorType sensor, PayloadData didRead, TargetIdentifier fromTarget) {
identifierToPayload.put(fromTarget, didRead.shortName());
add(fromTarget);
}
@Override
public void sensor(SensorType sensor, Proximity didMeasure, TargetIdentifier fromTarget) {
//add(fromTarget);
}
@Override
public void sensor(SensorType sensor, List didShare, TargetIdentifier fromTarget) {
for (PayloadData payload : didShare) {
add(payload.shortName());
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/data/TextFile.java
================================================
package com.ABC.pioneer.sensor.data;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.os.Environment;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TextFile {
private final File file;
public TextFile(final Context context, final String filename) {
final File folder = new File(getRootFolder(context), "Sensor");
file = new File(folder, filename);
final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);
}
}, 30, 30, TimeUnit.SECONDS);
}
/// 获取文件内容
public synchronized String contentsOf() {
try {
final FileInputStream fileInputStream = new FileInputStream(file);
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
final StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
bufferedReader.close();
fileInputStream.close();
return stringBuilder.toString();
} catch (Throwable e) {
return "";
}
}
/**
* 获取SD卡或模拟外部存储的根文件夹。
*/
private static File getRootFolder(final Context context) {
// 获取SD卡或模拟的外部存储。
// 在模拟存储后报告SD卡,因此请选择最后一个文件夹
final File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
return externalMediaDirs[externalMediaDirs.length - 1];
} else {
return Environment.getExternalStorageDirectory();
}
}
public synchronized boolean empty() {
return !file.exists() || file.length() == 0;
}
/// 将行追加到新文件或现有文件
public synchronized void write(String line) {
try {
final FileOutputStream fileOutputStream = new FileOutputStream(file, true);
fileOutputStream.write((line + "\n").getBytes());
fileOutputStream.flush();
fileOutputStream.close();
} catch (Throwable e) {
}
}
/// 覆盖文件内容
public synchronized void overwrite(String content) {
try {
final FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(content.getBytes());
fileOutputStream.flush();
fileOutputStream.close();
} catch (Throwable e) {
}
}
/// CSV输出的报价值(如果需要)。
public static String csv(String value) {
if (value.contains(",") || value.contains("\"") || value.contains("'") || value.contains("’")) {
return "\"" + value + "\"";
} else {
return value;
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Base64.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.io.ByteArrayOutputStream;
/// 不依赖Android API 26+的Base64编码和解码
public class Base64 {
private final static char[] encodeTable = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
':', '-'};
private final static int[] decodeTable = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2,
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51};
public static String encode(byte[] data) {
final StringBuilder buffer = new StringBuilder();
int pad = 0;
for (int i = 0; i < data.length; i += 3) {
int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF;
if (i + 1 < data.length) {
b |= (data[i + 1] & 0xFF) << 8;
} else {
pad++;
}
if (i + 2 < data.length) {
b |= (data[i + 2] & 0xFF);
} else {
pad++;
}
for (int j = 0; j < 4 - pad; j++) {
int c = (b & 0xFC0000) >> 18;
buffer.append(encodeTable[c]);
b <<= 6;
}
}
for (int j = 0; j < pad; j++) {
buffer.append("=");
}
return buffer.toString();
}
public static byte[] decode(String data) {
final byte[] bytes = data.getBytes();
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (int i = 0; i < bytes.length; ) {
int b = 0;
if (bytes[i] >=0 && bytes[i] < decodeTable.length && decodeTable[bytes[i]] != -1) {
b = (decodeTable[bytes[i]] & 0xFF) << 18;
} else {
// 跳过未知字符
i++;
continue;
}
int num = 0;
if (i + 1 < bytes.length && decodeTable[bytes[i + 1]] != -1) {
b = b | ((decodeTable[bytes[i + 1]] & 0xFF) << 12);
num++;
}
if (i + 2 < bytes.length && decodeTable[bytes[i + 2]] != -1) {
b = b | ((decodeTable[bytes[i + 2]] & 0xFF) << 6);
num++;
}
if (i + 3 < bytes.length && decodeTable[bytes[i + 3]] != -1) {
b = b | (decodeTable[bytes[i + 3]] & 0xFF);
num++;
}
while (num > 0) {
int c = (b & 0xFF0000) >> 16;
buffer.write((char) c);
b <<= 8;
num--;
}
i += 4;
}
return buffer.toByteArray();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/BluetoothState.java
================================================
package com.ABC.pioneer.sensor.datatype;
public enum BluetoothState {
unsupported, poweredOn, poweredOff, resetting
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Calibration.java
================================================
package com.ABC.pioneer.sensor.datatype;
import androidx.annotation.NonNull;
import java.util.Objects;
/// 用于解释传感器与目标之间接近值的校准数据,例如发射BLE的功率。
public class Calibration {
/// 度量单位,例如:发射功率
public final CalibrationMeasurementUnit unit;
/// 测量值 BLE广告中的发射功率
public final Double value;
public Calibration(CalibrationMeasurementUnit unit, Double value) {
this.unit = unit;
this.value = value;
}
/// 获取校准数据的纯文本描述
public String description() {
return unit + ":" + value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Calibration that = (Calibration) o;
return unit == that.unit &&
Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(unit, value);
}
@NonNull
@Override
public String toString() {
return description();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/CalibrationMeasurementUnit.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 测量单元,用于校准接近传输数据值,例如 BLE发射功率
public enum CalibrationMeasurementUnit {
/// 蓝牙发射功率,用于描述1米处的预期RSSI,以解释测量的RSSI值。
BLETransmitPower
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Callback.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 通用回调函数
public interface Callback {
void accept(T value);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Data.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.nio.charset.Charset;
import java.util.Arrays;
/// 原始字节数组数据
public class Data {
private final static char[] hexChars = "0123456789ABCDEF".toCharArray();
public byte[] value = null;
public Data() {
this(new byte[0]);
}
public Data(byte[] value) {
this.value = value;
}
public Data(final Data data) {
final byte[] value = new byte[data.value.length];
System.arraycopy(data.value, 0, value, 0, data.value.length);
this.value = value;
}
public Data(byte repeating, int count) {
this.value = new byte[count];
for (int i=count; i-->0;) {
this.value[i] = repeating;
}
}
public Data(String base64EncodedString) {
this.value = Base64.decode(base64EncodedString);
}
public String base64EncodedString() {
return Base64.encode(value);
}
public String hexEncodedString() {
if (value == null) {
return "";
}
final StringBuilder stringBuilder = new StringBuilder(value.length * 2);
for (int i = 0; i < value.length; i++) {
final int v = value[i] & 0xFF;
stringBuilder.append(hexChars[v >>> 4]);
stringBuilder.append(hexChars[v & 0x0F]);
}
return stringBuilder.toString();
}
public final static Data fromHexEncodedString(String hexEncodedString) {
final int length = hexEncodedString.length();
final byte[] value = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
value[i / 2] = (byte) ((Character.digit(hexEncodedString.charAt(i), 16) << 4) +
Character.digit(hexEncodedString.charAt(i+1), 16));
}
return new Data(value);
}
public String description() {
return base64EncodedString();
}
/// 获取从偏移量到结束的子数据
public Data subdata(int offset) {
if (offset >=0 && offset < value.length) {
final byte[] offsetValue = new byte[value.length - offset];
System.arraycopy(value, offset, offsetValue, 0, offsetValue.length);
return new Data(offsetValue);
} else {
return null;
}
}
/// 获取偏移量到偏移量+长度的子数据
public Data subdata(int offset, int length) {
if (offset >= 0 && offset < value.length && offset + length <= value.length) {
final byte[] offsetValue = new byte[length];
System.arraycopy(value, offset, offsetValue, 0, length);
return new Data(offsetValue);
} else {
return null;
}
}
/// 将数据附加到此数据的末尾。
public void append(Data data) {
append(data.value);
}
private void append(byte[] data) {
final byte[] concatenated = new byte[data.length + value.length];
System.arraycopy(value, 0, concatenated, 0, value.length);
System.arraycopy(data, 0, concatenated, value.length, data.length);
value = concatenated;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Data data = (Data) o;
return Arrays.equals(value, data.value);
}
@Override
public int hashCode() {
return Arrays.hashCode(value);
}
@Override
public String toString() {
return hexEncodedString();
}
// 从内在类型到数据的转换
public void append(final UInt8 value) {
append(new byte[]{
(byte) (value.value & 0xFF)
});
}
public UInt8 uint8(final int index) {
if (index < 0 || index >= value.length) {
return null;
}
return new UInt8(value[index] & 0xFF);
}
public void append(final Int8 value) {
append(new byte[]{
(byte) (value.value & 0xFF)
});
}
public Int8 int8(final int index) {
if (index < 0 || index >= value.length) {
return null;
}
return new Int8(value[index]);
}
public void append(final UInt16 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) ((value.value >>> 8) & 0xFF) // 最高位
});
}
public UInt16 uint16(final int index) {
if (index < 0 || index + 1 >= value.length) {
return null;
}
final int v =
value[index] & 0xFF |
((value[index + 1] & 0xFF) << 8);
return new UInt16(v);
}
public void append(final Int16 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) (value.value >> 8) // 最高位
});
}
public Int16 int16(final int index) {
if (index < 0 || index + 1 >= value.length) {
return null;
}
final int v =
value[index] & 0xFF |
((value[index + 1]) << 8);
return new Int16(v);
}
public void append(final UInt32 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) ((value.value >>> 8) & 0xFF),
(byte) ((value.value >>> 16) & 0xFF),
(byte) ((value.value >>> 24) & 0xFF) // 最高位
});
}
public UInt32 uint32(final int index) {
if (index < 0 || index + 3 >= value.length) {
return null;
}
final long v =
(long) (value[index] & 0xFF) |
((long) (value[index + 1] & 0xFF) << 8) |
((long) (value[index + 2] & 0xFF) << 16) |
((long) (value[index + 3] & 0xFF) << 24);
return new UInt32(v);
}
public void append(final Int32 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) ((value.value >> 8) & 0xFF),
(byte) ((value.value >> 16) & 0xFF),
(byte) (value.value >> 24) // 最高位
});
}
public Int32 int32(final int index) {
if (index < 0 || index + 3 >= value.length) {
return null;
}
final int v =
(value[index] & 0xFF) |
((value[index + 1] & 0xFF) << 8) |
((value[index + 2] & 0xFF) << 16) |
((value[index + 3]) << 24);
return new Int32(v);
}
public void append(final UInt64 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) ((value.value >>> 8) & 0xFF),
(byte) ((value.value >>> 16) & 0xFF),
(byte) ((value.value >>> 24) & 0xFF),
(byte) ((value.value >>> 32) & 0xFF),
(byte) ((value.value >>> 40) & 0xFF),
(byte) ((value.value >>> 48) & 0xFF),
(byte) ((value.value >>> 56) & 0xFF) // 最高位
});
}
public UInt64 uint64(final int index) {
if (index < 0 || index + 7 >= value.length) {
return null;
}
final long v =
(long) value[index] & 0xFF |
((long) (value[index + 1] & 0xFF) << 8) |
((long) (value[index + 2] & 0xFF) << 16) |
((long) (value[index + 3] & 0xFF) << 24) |
((long) (value[index + 4] & 0xFF) << 32) |
((long) (value[index + 5] & 0xFF) << 40) |
((long) (value[index + 6] & 0xFF) << 48) |
((long) (value[index + 7] & 0xFF) << 56);
return new UInt64(v);
}
public void append(final Int64 value) {
append(new byte[]{
(byte) (value.value & 0xFF), // 最低位
(byte) ((value.value >> 8) & 0xFF),
(byte) ((value.value >> 16) & 0xFF),
(byte) ((value.value >> 24) & 0xFF),
(byte) ((value.value >> 32) & 0xFF),
(byte) ((value.value >> 40) & 0xFF),
(byte) ((value.value >> 48) & 0xFF),
(byte) ((value.value >> 56)) // 最高位
});
}
public Int64 int64(final int index) {
if (index < 0 || index + 7 >= value.length) {
return null;
}
final long v =
(long) value[index] & 0xFF |
((long) (value[index + 1] & 0xFF) << 8) |
((long) (value[index + 2] & 0xFF) << 16) |
((long) (value[index + 3] & 0xFF) << 24) |
((long) (value[index + 4] & 0xFF) << 32) |
((long) (value[index + 5] & 0xFF) << 40) |
((long) (value[index + 6] & 0xFF) << 48) |
((long) (value[index + 7]) << 56);
return new Int64(v);
}
public void append(Float16 value) {
append(value.bigEndian);
}
public Float16 float16(int index) {
if (index < 0 || index + 1 >= value.length) {
return null;
}
return new Float16(new Data(new byte[] {
value[index], value[index + 1]
}));
}
// 到(从)数据函数的字符串
/// 字符串长度数据的编码选项作为前缀
public enum StringLengthEncodingOption {
UINT8, UINT16, UINT32, UINT64
}
/// 将字符串编码为数据,使用UInt8,...,64将长度作为前缀插入。 如果成功,则返回true,否则返回false。
public boolean append(final String value) {
return append(value, StringLengthEncodingOption.UINT8);
}
public boolean append(final String value, final StringLengthEncodingOption encoding) {
if (value == null) {
return false;
}
byte[] data = null;
try {
data = value.getBytes("UTF-8");
} catch (Throwable e) {
return false;
}
if (data == null) {
return false;
}
switch (encoding) {
case UINT8:
if (!(data.length <= UInt8.max.value)) {
return false;
}
append(new UInt8(data.length));
break;
case UINT16:
if (!(data.length <= UInt16.max.value)) {
return false;
}
append(new UInt16(data.length));
break;
case UINT32:
if (!(data.length <= UInt32.max.value)) {
return false;
}
append(new UInt32(data.length));
break;
case UINT64:
if (!(data.length <= UInt64.max.value)) {
return false;
}
append(new UInt64(data.length));
break;
}
append(data);
return true;
}
/// 数据字节数组中已解码的字符串和开始/结束索引
public final static class DecodedString {
public final String value;
public final int start;
public final int end;
public DecodedString(final String value, final int start, final int end) {
this.value = value;
this.start = start;
this.end = end;
}
}
public DecodedString string(final int index) {
return string(index, StringLengthEncodingOption.UINT8);
}
public DecodedString string(final int index, final StringLengthEncodingOption encoding) {
long start = index;
long end = index;
switch (encoding) {
case UINT8: {
final UInt8 count = uint8(index);
if (count == null) {
return null;
}
start = index + 1;
end = start + count.value;
break;
}
case UINT16: {
final UInt16 count = uint16(index);
if (count == null) {
return null;
}
start = index + 2;
end = start + count.value;
break;
}
case UINT32: {
final UInt32 count = uint32(index);
if (count == null) {
return null;
}
start = index + 4;
end = start + count.value;
break;
}
case UINT64: {
final UInt64 count = uint64(index);
if (count == null) {
return null;
}
start = index + 8;
end = start + count.value;
break;
}
}
if (start > Integer.MAX_VALUE || end > Integer.MAX_VALUE) {
return null;
}
if (start == index || start > value.length || end > value.length) {
return null;
}
try {
final String string = new String(value, (int) start, (int) (end - start), Charset.forName("UTF-8"));
return new DecodedString(string, (int) start, (int) end);
} catch (Throwable e) {
return null;
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Encounter.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.text.SimpleDateFormat;
import java.util.Date;
/// 遇到记录,描述某一时刻与目标的接近程度
public class Encounter {
private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public Date timestamp = null;
public Proximity proximity = null;
public PayloadData payload = null;
public Encounter(Proximity didMeasure, PayloadData withPayload, Date timestamp) {
this.timestamp = timestamp;
this.proximity = didMeasure;
this.payload = withPayload;
}
public Encounter(Proximity didMeasure, PayloadData withPayload) {
this(didMeasure, withPayload, new Date());
}
public Encounter(String row) {
final String[] fields = row.split(",", -1);
if (!(fields.length >= 6)) {
return;
}
try {
if (fields[0] != null && !fields[0].isEmpty()) {
this.timestamp = dateFormatter.parse(fields[0]);
}
} catch (Throwable e) {
}
Calibration calibration = null;
try {
if (fields[3] != null && fields[4] != null && !fields[3].isEmpty() && !fields[4].isEmpty()) {
final double calibrationValue = Double.parseDouble(fields[3]);
final CalibrationMeasurementUnit calibrationUnit = CalibrationMeasurementUnit.valueOf(fields[4]);
calibration = new Calibration(calibrationUnit, calibrationValue);
}
} catch (Throwable e) {
}
try {
if (fields[1] != null && fields[2] != null && !fields[1].isEmpty() && !fields[2].isEmpty()) {
final double proximityValue = Double.parseDouble(fields[1]);
final ProximityMeasurementUnit proximityUnit = ProximityMeasurementUnit.valueOf(fields[2]);
this.proximity = new Proximity(proximityUnit, proximityValue, calibration);
}
} catch (Throwable e) {
}
if (fields[5] != null) {
this.payload = new PayloadData(fields[5]);
}
}
public String csvString() {
final String f0 = (timestamp == null ? "" : dateFormatter.format(timestamp));
final String f1 = (proximity == null || proximity.value == null ? "" : proximity.value.toString());
final String f2 = (proximity == null || proximity.unit == null ? "" : proximity.unit.name());
final String f3 = (proximity == null || proximity.calibration == null || proximity.calibration.value == null ? "" : proximity.calibration.value.toString());
final String f4 = (proximity == null || proximity.calibration == null || proximity.calibration.unit == null ? "" : proximity.calibration.unit.name());
final String f5 = (payload == null ? "" : payload.base64EncodedString());
return f0 + "," + f1 + "," + f2 + "," + f3 + "," + f4 + "," + f5;
}
public boolean isValid() {
return timestamp != null && proximity != null && payload != null;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Float16.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/// IEEE 754二进制16格式16位浮点数
public class Float16 {
public final float value;
public final Data bigEndian;
public Float16(float value) {
this.value = value;
final ByteBuffer byteBuffer = ByteBuffer.allocate(2);
byteBuffer.order(ByteOrder.BIG_ENDIAN);
byteBuffer.putShort((short) float16(value));
this.bigEndian = new Data(byteBuffer.array());
}
public Float16(Data bigEndian) {
this.bigEndian = bigEndian;
final ByteBuffer byteBuffer = ByteBuffer.wrap(bigEndian.value);
byteBuffer.order(ByteOrder.BIG_ENDIAN);
final int value = byteBuffer.getShort(0);
this.value = valueOf(value);
}
/// IEEE 754二进制16格式16位浮点数
private static int float16(float values) {
final int bits = Float.floatToIntBits(values);
final int sign = bits >>> 16 & 0x8000;
int val = (bits & 0x7fffffff) + 0x1000;
if (val >= 0x47800000) {
if ((bits & 0x7fffffff) >= 0x47800000) {
if (val < 0x7f800000) {
return sign | 0x7c00;
}
return sign | 0x7c00 | (bits & 0x007fffff) >>> 13;
}
return sign | 0x7bff;
}
if (val >= 0x38800000) {
return sign | val - 0x38000000 >>> 13;
}
if (val < 0x33000000) {
return sign;
}
val = (bits & 0x7fffffff) >>> 23;
return sign | ((bits & 0x7fffff | 0x800000) + (0x800000 >>> val - 102) >>> 126 - val);
}
private static float valueOf(int hbits) {
int mant = hbits & 0x03ff;
int exp = hbits & 0x7c00;
if (exp == 0x7c00) {
exp = 0x3fc00;
} else if (exp != 0) {
exp += 0x1c000;
if (mant == 0 && exp > 0x1c400) {
return Float.intBitsToFloat((hbits & 0x8000) << 16 | exp << 13 | 0x3ff);
}
} else if (mant != 0) {
exp = 0x1c400;
do {
mant <<= 1;
exp -= 0x400;
} while ((mant & 0x400) == 0);
mant &= 0x3ff;
}
return Float.intBitsToFloat((hbits & 0x8000) << 16 | (exp | mant) << 13);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/ImmediateSendData.java
================================================
package com.ABC.pioneer.sensor.datatype;
public class ImmediateSendData {
public final Data data;
// 立即传输数据
public ImmediateSendData(final Data data) {
this.data = data;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/InertiaLocationReference.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 时间点上的加速度(x,y,z)以米/秒为单位
public class InertiaLocationReference implements LocationReference {
public final double x, y, z, magnitude;
public InertiaLocationReference(Double x, Double y, Double z) {
this.x = (x == null ? 0 : x);
this.y = (y == null ? 0 : y);
this.z = (z == null ? 0 : z);
this.magnitude = Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
}
public String description() {
return "Inertia(magnitude=" + magnitude + ",x=" + x + ",y=" + y + ",z=" + z + ")";
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Int16.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 有符号整型 (16 位)
public class Int16 {
public final static int bitWidth = 16;
public final static Int16 min = new Int16(Short.MIN_VALUE);
public final static Int16 max = new Int16(Short.MAX_VALUE);
public final int value;
public Int16(int value) {
this.value = (value < Short.MIN_VALUE ? Short.MIN_VALUE : (value > Short.MAX_VALUE ? Short.MAX_VALUE : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Int16 uInt16 = (Int16) o;
return value == uInt16.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Integer.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Int32.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 有符号整型 (32位)
public class Int32 {
public final static int bitWidth = 32;
public final static Int32 min = new Int32(Integer.MIN_VALUE);
public final static Int32 max = new Int32(Integer.MAX_VALUE);
public final int value;
public Int32(long value) {
this.value = (int) (value < Integer.MIN_VALUE ? Integer.MIN_VALUE : (value > Integer.MAX_VALUE ? Integer.MAX_VALUE : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Int32 uInt32 = (Int32) o;
return value == uInt32.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Integer.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Int64.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 有符号整型 (64位)
public class Int64 {
public final static int bitWidth = 64;
public final static Int64 min = new Int64(Long.MIN_VALUE);
public final static Int64 max = new Int64(Long.MAX_VALUE);
public final long value;
public Int64(long value) {
this.value = (value < Long.MIN_VALUE ? Long.MIN_VALUE : (value > Long.MAX_VALUE ? Long.MAX_VALUE : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Int64 uInt64 = (Int64) o;
return value == uInt64.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Long.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Int8.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 有符号整型(8位)
public class Int8 {
public final static int bitWidth = 8;
public final static Int8 min = new Int8(Byte.MIN_VALUE);
public final static Int8 max = new Int8(Byte.MAX_VALUE);
public final int value;
public Int8(int value) {
this.value = (value < Byte.MIN_VALUE ? Byte.MIN_VALUE : (value > Byte.MAX_VALUE ? Byte.MAX_VALUE : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Int8 uInt8 = (Int8) o;
return value == uInt8.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Integer.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/LegacyPayload.java
================================================
package com.ABC.pioneer.sensor.datatype;
import com.ABC.pioneer.sensor.ble.Configurations;
import java.util.UUID;
/// 从目标接收到的旧有效负载数据
public class LegacyPayload extends PayloadData {
public final UUID service;
public enum ProtocolName {
UNKNOWN, NOT_AVAILABLE, PIONEER
}
public LegacyPayload(final UUID service, final byte[] value) {
super(value);
this.service = service;
}
public ProtocolName protocolName() {
if (service == null) {
return ProtocolName.NOT_AVAILABLE;
} else if (service == Configurations.serviceUUID) {
return ProtocolName.PIONEER;
} else {
return ProtocolName.UNKNOWN;
}
}
@Override
public String shortName() {
return super.shortName();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Location.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Date;
/// 原始位置数据,用于估算间接接触
public class Location {
/// 测量值,例如 GPS坐标以逗号分隔的字符串格式表示纬度和经度
public final LocationReference value;
/// 在位置上花费的时间。
public final Date start, end;
public Location(LocationReference value, Date start, Date end) {
this.value = value;
this.start = start;
this.end = end;
}
/// 获取邻近数据的纯文本描述
public String description() {
return (value == null ? "null" : value.description()) + ":[from=" + start + ",to=" + end + "]";
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/LocationReference.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 用于估算间接暴露的原始位置数据,例如 WGS84坐标
public interface LocationReference {
String description();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/PayloadData.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 从目标接收到的加密有效负载数据。 这很可能是目标的实际永久标识符的加密数据报。
public class PayloadData extends Data {
public PayloadData(byte[] value) {
super(value);
}
public PayloadData(String base64EncodedString) {
super(base64EncodedString);
}
public PayloadData(byte repeating, int count) {
super(repeating, count);
}
public PayloadData() {
this(new byte[0]);
}
public String shortName() {
if (value.length == 0) {
return "";
}
if (!(value.length > 3)) {
return Base64.encode(value);
}
final Data subdata = subdata(3, value.length - 3);
final byte[] suffix = (subdata == null || subdata.value == null ? new byte[0] : subdata.value);
final String base64EncodedString = Base64.encode(suffix);
return base64EncodedString.substring(0, Math.min(6, base64EncodedString.length()));
}
public String toString() {
return shortName();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/PayloadSharingData.java
================================================
package com.ABC.pioneer.sensor.datatype;
public class PayloadSharingData {
public final RSSI rssi;
public final Data data;
public PayloadSharingData(final RSSI rssi, final Data data) {
this.rssi = rssi;
this.data = data;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/PayloadTimestamp.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Date;
/// 有效负载时间戳记通常应为Date,但将来可能会更改为UInt64以使用服务器同步的相对时间戳记。
public class PayloadTimestamp {
public final Date value;
public PayloadTimestamp(Date value) {
this.value = value;
}
public PayloadTimestamp() {
this(new Date());
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/PlacenameLocationReference.java
================================================
package com.ABC.pioneer.sensor.datatype;
public class PlacenameLocationReference implements LocationReference {
public final String name;
public PlacenameLocationReference(String name) {
this.name = name;
}
public String description() {
return "PLACE(name=" + name + ")";
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Proximity.java
================================================
package com.ABC.pioneer.sensor.datatype;
import androidx.annotation.NonNull;
import java.util.Objects;
/// 用于估算传感器与目标之间接近程度的原始数据,例如 BLE的RSSI。
public class Proximity {
/// 度量单位,例如:RSSI
public final ProximityMeasurementUnit unit;
/// 测量值,例如:原始RSSI值。
public final Double value;
/// 校准数据(可选),例如:发射功率
public final Calibration calibration;
public Proximity(ProximityMeasurementUnit unit, Double value) {
this(unit, value, null);
}
public Proximity(ProximityMeasurementUnit unit, Double value, Calibration calibration) {
this.unit = unit;
this.value = value;
this.calibration = calibration;
}
/// 获取邻近数据的纯文本描述
public String description() {
if (calibration == null) {
return unit + ":" + value;
}
return unit + ":" + value + "[" + calibration.description() + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Proximity proximity = (Proximity) o;
return unit == proximity.unit &&
Objects.equals(value, proximity.value) &&
Objects.equals(calibration, proximity.calibration);
}
@Override
public int hashCode() {
return Objects.hash(unit, value, calibration);
}
@NonNull
@Override
public String toString() {
return description();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/ProximityMeasurementUnit.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 用于解释邻近数据值的测量单元。
public enum ProximityMeasurementUnit {
/// 接收信号强度指示,例如 BLE信号强度作为接近度估算器。
RSSI,
/// 往返时间,例如 音频信号回声持续时间作为接近度估算器。
RTT
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/PseudoDeviceAddress.java
================================================
package com.ABC.pioneer.sensor.datatype;
import com.ABC.pioneer.sensor.ble.Configurations;
import java.util.Objects;
/// 伪设备地址,可在不依赖设备mac地址的情况下缓存设备有效负载可能会经常更改,例如A10和A20。
public class PseudoDeviceAddress {
private static RandomSource randomSource = new RandomSource(RandomSource.Method.Random);
public final long address;
public final byte[] data;
public PseudoDeviceAddress(final RandomSource.Method method) {
// 蓝牙设备地址为48位(6字节),使用相同的长度以提供相同的避免冲突的选择。随机,安全随机单例,
// 安全随机和符合NIST的安全随机之间进行选择,因为随机源随机是非阻塞的并且具有 在这种情况下,
// 我们被修改为从可靠来源获得熵。 为此目的,它经过验证和推荐是足够安全的。
// -SecureRandomSingleton在闲置设备上运行4-8小时后会阻塞,并且不适用于此用例,不建议使用
// -SecureRandom在闲置设备上运行4-8小时后会阻塞,不适合此使用案例,不建议使用
// -SecureRandomNIST在闲置设备上运行6个小时后会被阻止,因此不适用于此用例,不建议使用
if (randomSource == null || randomSource.method != method) {
randomSource = new RandomSource(method);
}
this.data = encode(randomSource.nextLong());
this.address = decode(this.data);
}
/// 默认构造函数使用Random作为随机源,请参见上文和RandomSource了解详细信息。
public PseudoDeviceAddress() {
this(Configurations.pseudoDeviceAddressRandomisation);
}
public PseudoDeviceAddress(final byte[] data) {
this.address = decode(data);
this.data = encode(this.address);
}
public PseudoDeviceAddress(final long value) {
this.data = encode(value);
this.address = decode(data);
}
protected final static byte[] encode(final long value) {
final Data encoded = new Data();
encoded.append(new Int64(value));
return encoded.subdata(2, 6).value;
}
protected final static long decode(final byte[] data) {
final Data decoded = new Data((byte) 0, 2);
decoded.append(new Data(data));
if (decoded.value.length < 8) {
decoded.append(new Data((byte) 0, 8 - decoded.value.length));
}
final Int64 int64 = decoded.int64(0);
return (int64 == null ? 0 : decoded.int64(0).value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PseudoDeviceAddress that = (PseudoDeviceAddress) o;
return address == that.address;
}
@Override
public int hashCode() {
return Objects.hash(address);
}
@Override
public String toString() {
return Base64.encode(data);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/RSSI.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// RSSI单位为dBm。
public class RSSI {
public final int value;
public RSSI(int value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RSSI rssi = (RSSI) o;
return value == rssi.value;
}
@Override
public int hashCode() {
return value;
}
@Override
public String toString() {
return "RSSI{" +
"value=" + value +
'}';
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/RandomSource.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.security.SecureRandom;
import java.util.Random;
/// 随机数据来源
public class RandomSource {
public final Method method;
private Random random = null;
private short externalEntropy = 0;
public enum Method {
// 每次调用可重用单例随机源
Random, SecureRandomSingleton, SecureRandom, SecureRandomNIST
}
public RandomSource(final Method method) {
this.method = method;
externalEntropy = (short) (Math.random() * Short.MAX_VALUE);
}
/// 从外部来源贡献熵,例如 不可预测的时间间隔
public synchronized void addEntropy(final long value) {
final short contribution = (short) (value % Short.MAX_VALUE);
externalEntropy += contribution;
}
// MARK: 随机数据
public void nextBytes(byte[] bytes) {
init();
random.nextBytes(bytes);
}
public int nextInt() {
init();
return random.nextInt();
}
public long nextLong() {
init();
return random.nextLong();
}
public double nextDouble() {
init();
return random.nextDouble();
}
/// 根据方法初始化随机
protected synchronized void init() {
switch (method) {
case Random: {
random = getRandom();
break;
}
case SecureRandomSingleton: {
random = getSecureRandomSingleton();
break;
}
case SecureRandom: {
random = getSecureRandom();
break;
}
case SecureRandomNIST: {
random = getSecureRandomNIST();
break;
}
}
// 使用外部熵调整PRNG序列位置0-128位
final int skipPositions = (Math.abs(externalEntropy) % 128);
for (int i=skipPositions; i-->0;) {
random.nextBoolean();
}
externalEntropy = 0;
}
/// 具有可靠熵源的无阻塞随机数生成器。
private static long getRandomLongLastCalledAt = System.nanoTime();
private synchronized final static Random getRandom() {
//在调用之间使用不可预测的时间来增加熵
final long timestamp = System.nanoTime();
final long entropy = (timestamp - getRandomLongLastCalledAt);
final int skipRandomSequence = (int) Math.abs(entropy % 128);
for (int i=skipRandomSequence; i-->0;) {
Math.random();
}
// 使用Math.random()中的种子创建一个Random新实例,以增加搜索空间
// 从新的Random实例获得的值到Math.random()的种子。
final Random random = new Random(Math.round(Math.random() * Long.MAX_VALUE));
// 在新的Random实例上跳过256-1280位,以将搜索空间从新的Random实例值增加到其种子。
// 使用Math.random()选择跳过距离以增加搜索空间。
final int skipInitialBits = (int) Math.abs(256 + Math.round(Math.random() * 1024));
for (int i=skipInitialBits; i-->0;) {
random.nextBoolean();
}
// 更新时间戳以在下一个呼叫中使用
getRandomLongLastCalledAt = timestamp;
// 获取下一个long
return random;
}
/// 安全的随机数生成器,由于缺少熵,在闲置设备上运行约7.5小时后会阻塞。
private static SecureRandom secureRandomSingleton = null;
private synchronized final static Random getSecureRandomSingleton() {
if (secureRandomSingleton == null) {
secureRandomSingleton = new SecureRandom();
}
return secureRandomSingleton;
}
/// 安全的随机数生成器,由于缺少熵,在空闲设备上约4.5小时后会阻塞。
private final static Random getSecureRandom() {
return new SecureRandom();
}
private final static Random getSecureRandomNIST() {
try {
final SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
final SecureRandom secureRandomForSeed = new SecureRandom();
final byte[] seed = secureRandomForSeed.generateSeed(55);
secureRandom.setSeed(seed); // seed with random number
secureRandom.nextBytes(new byte[256 + secureRandom.nextInt(1024)]);
return secureRandom;
} catch (Throwable e) {
return getSecureRandom();
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/SensorState.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 传感器状态
public enum SensorState {
/// 传感器开启,处于活动状态且可运行
on,
///传感器已关闭,处于不活动状态并且无法运行
off,
/// 传感器不可用
unavailable
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/SensorType.java
================================================
package com.ABC.pioneer.sensor.datatype;
/// 传感器类型作为目标标识符的限定符
public enum SensorType {
/// 低功耗蓝牙(BLE)
BLE,
/// 加速度计运动传感器
ACCELEROMETER
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/SignalCharacteristicData.java
================================================
package com.ABC.pioneer.sensor.datatype;
import com.ABC.pioneer.sensor.ble.Configurations;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/// 信号特征数据包的编解码器
public class SignalCharacteristicData {
/// 编码写入RSSI数据包
// writeRSSI数据格式
// 0-0 : 动作码
// 1-2 : rssi值(Int16)
public static Data encodeWriteRssi(final RSSI rssi) {
final ByteBuffer byteBuffer = ByteBuffer.allocate(3);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(0, Configurations.signalCharacteristicActionWriteRSSI);
byteBuffer.putShort(1, (short) rssi.value);
return new Data(byteBuffer.array());
}
/// 解码写入RSSI数据包
public static RSSI decodeWriteRSSI(final Data data) {
if (data == null || data.value == null) {
return null;
}
if (signalDataActionCode(data.value) != Configurations.signalCharacteristicActionWriteRSSI) {
return null;
}
if (data.value.length != 3) {
return null;
}
final Short rssiValue = int16(data.value, 1);
if (rssiValue == null) {
return null;
}
return new RSSI(rssiValue.intValue());
}
/// 编码写入payload数据包
// writePayload数据格式
// 0-0 : 动作码
// 1-2 : payload数据计数(以字节为单位)(Int16)
// 3.. : payload数据
public static Data encodeWritePayload(final PayloadData payloadData) {
final ByteBuffer byteBuffer = ByteBuffer.allocate(3 + payloadData.value.length);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(0, Configurations.signalCharacteristicActionWritePayload);
byteBuffer.putShort(1, (short) payloadData.value.length);
byteBuffer.position(3);
byteBuffer.put(payloadData.value);
return new Data(byteBuffer.array());
}
/// 解码写入有效负载数据包
public static PayloadData decodeWritePayload(final Data data) {
if (data == null || data.value == null) {
return null;
}
if (signalDataActionCode(data.value) != Configurations.signalCharacteristicActionWritePayload) {
return null;
}
if (data.value.length < 3) {
return null;
}
final Short payloadDataCount = int16(data.value, 1);
if (payloadDataCount == null) {
return null;
}
if (payloadDataCount == 0) {
return new PayloadData();
}
if (data.value.length != (3 + payloadDataCount.intValue())) {
return null;
}
final Data payloadDataBytes = new Data(data.value).subdata(3);
if (payloadDataBytes == null) {
return null;
}
return new PayloadData(payloadDataBytes.value);
}
/// 编码写入有效负载共享数据包
// writePayloadSharing数据格式
// 0-0 : 动作码
// 1-2 : rssi值(Int16)
// 3-4 : payload共享数据 (Int16)
// 5.. : payload sharing 数据值
public static Data encodeWritePayloadSharing(final PayloadSharingData payloadSharingData) {
final ByteBuffer byteBuffer = ByteBuffer.allocate(5 + payloadSharingData.data.value.length);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(0, Configurations.signalCharacteristicActionWritePayloadSharing);
byteBuffer.putShort(1, (short) payloadSharingData.rssi.value);
byteBuffer.putShort(3, (short) payloadSharingData.data.value.length);
byteBuffer.position(5);
byteBuffer.put(payloadSharingData.data.value);
return new Data(byteBuffer.array());
}
/// 解码写入有效负载数据包
public static PayloadSharingData decodeWritePayloadSharing(final Data data) {
if (data == null || data.value == null) {
return null;
}
if (signalDataActionCode(data.value) != Configurations.signalCharacteristicActionWritePayloadSharing) {
return null;
}
if (data.value.length < 5) {
return null;
}
final Short rssiValue = int16(data.value, 1);
if (rssiValue == null) {
return null;
}
final Short payloadSharingDataCount = int16(data.value, 3);
if (payloadSharingDataCount == null) {
return null;
}
if (payloadSharingDataCount == 0) {
return new PayloadSharingData(new RSSI(rssiValue.intValue()), new Data());
}
if (data.value.length != (5 + payloadSharingDataCount.intValue())) {
return null;
}
final Data payloadSharingDataBytes = new Data(data.value).subdata(5);
if (payloadSharingDataBytes == null) {
return null;
}
return new PayloadSharingData(new RSSI(rssiValue.intValue()), payloadSharingDataBytes);
}
/// 编码立即发送数据包
// 立即发送数据格式
// 0-0 : 动作码
// 1-2 : payload数据计数(Int16)
// 3.. : payload data值
public static Data encodeImmediateSend(final ImmediateSendData immediateSendData) {
final ByteBuffer byteBuffer = ByteBuffer.allocate(3 + immediateSendData.data.value.length);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(0, Configurations.signalCharacteristicActionWriteImmediate);
byteBuffer.putShort(1, (short) immediateSendData.data.value.length);
byteBuffer.position(3);
byteBuffer.put(immediateSendData.data.value);
return new Data(byteBuffer.array());
}
/// 解码立即发送数据包
public static ImmediateSendData decodeImmediateSend(final Data data) {
if (data == null || data.value == null) {
return null;
}
if (signalDataActionCode(data.value) != Configurations.signalCharacteristicActionWriteImmediate) {
return null;
}
if (data.value.length < 3) {
return null;
}
final Short immediateSendDataCount = int16(data.value, 1);
if (immediateSendDataCount == null) {
return null;
}
if (immediateSendDataCount == 0) {
return new ImmediateSendData(new Data());
}
if (data.value.length != (3 + immediateSendDataCount.intValue())) {
return null;
}
final Data immediateSendDataBytes = new Data(data.value).subdata(3);
if (immediateSendDataBytes == null) {
return null;
}
return new ImmediateSendData(immediateSendDataBytes);
}
/// 检测信号特征数据包类型
public static SignalCharacteristicDataType detect(Data data) {
switch (signalDataActionCode(data.value)) {
case Configurations.signalCharacteristicActionWriteRSSI:
return SignalCharacteristicDataType.rssi;
case Configurations.signalCharacteristicActionWritePayload:
return SignalCharacteristicDataType.payload;
case Configurations.signalCharacteristicActionWritePayloadSharing:
return SignalCharacteristicDataType.payloadSharing;
case Configurations.signalCharacteristicActionWriteImmediate:
return SignalCharacteristicDataType.immediateSend;
default:
return SignalCharacteristicDataType.unknown;
}
}
private static byte signalDataActionCode(byte[] signalData) {
if (signalData == null || signalData.length == 0) {
return 0;
}
return signalData[0];
}
private static Short int16(byte[] data, int index) {
if (index < data.length - 1) {
final ByteBuffer byteBuffer = ByteBuffer.wrap(data);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer.getShort(index);
} else {
return null;
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/SignalCharacteristicDataType.java
================================================
package com.ABC.pioneer.sensor.datatype;
public enum SignalCharacteristicDataType {
rssi, payload, payloadSharing, immediateSend, unknown
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/TargetIdentifier.java
================================================
package com.ABC.pioneer.sensor.datatype;
import android.bluetooth.BluetoothDevice;
import java.util.Objects;
import java.util.UUID;
/// 检测到的目标(例如智能手机,信标,地点)的临时标识符。
// 这可能是一个UUID,但使用String表示变量标识符长度。
public class TargetIdentifier {
public final String value;
protected TargetIdentifier(final String value) {
this.value = value;
}
/// 创建随机目标标识符
public TargetIdentifier() {
this(UUID.randomUUID().toString());
}
/// 根据蓝牙设备地址创建目标标识符
public TargetIdentifier(BluetoothDevice bluetoothDevice) {
this(bluetoothDevice.getAddress());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TargetIdentifier that = (TargetIdentifier) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/TimeInterval.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Date;
import java.util.Objects;
/// 时间间隔(以秒为单位)。
public class TimeInterval {
public final long value;
public static final TimeInterval minute = new TimeInterval(60);
public static final TimeInterval zero = new TimeInterval(0);
public static final TimeInterval never = new TimeInterval(Long.MAX_VALUE);
public TimeInterval(long seconds) {
this.value = seconds;
}
public TimeInterval(Date date) {
this.value = date.getTime() / 1000;
}
public TimeInterval(Date from, Date to) {
this.value = (to.getTime() - from.getTime()) / 1000;
}
public static TimeInterval minutes(long minutes) {
return new TimeInterval(minute.value * minutes);
}
public static TimeInterval seconds(long seconds) {
return new TimeInterval(seconds);
}
public long millis() {
return value * 1000;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TimeInterval that = (TimeInterval) o;
return value == that.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
if (value == never.value) {
return "never";
}
return Long.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Triple.java
================================================
package com.ABC.pioneer.sensor.datatype;
public class Triple {
public final A a;
public final B b;
public final C c;
public Triple(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public String toString() {
return "Triple{" +
"a=" + a +
", b=" + b +
", c=" + c +
'}';
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/Tuple.java
================================================
package com.ABC.pioneer.sensor.datatype;
public class Tuple {
public final A a;
public final B b;
public Tuple(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public String toString() {
return "(" + a + "," + b + ")";
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/UInt16.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 无符号数整数(16位)
public class UInt16 {
public final static int bitWidth = 16;
public final static UInt16 min = new UInt16(0);
public final static UInt16 max = new UInt16(65535);
public final int value;
public UInt16(int value) {
this.value = (value < 0 ? 0 : (value > 65535 ? 65535 : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UInt16 uInt16 = (UInt16) o;
return value == uInt16.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Integer.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/UInt32.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 无符号数整数(32位)
public class UInt32 {
public final static int bitWidth = 32;
public final static UInt32 min = new UInt32(0);
public final static UInt32 max = new UInt32(4294967295l);
public final long value;
public UInt32(long value) {
this.value = (value < 0 ? 0 : (value > 4294967295l ? 4294967295l : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UInt32 uInt32 = (UInt32) o;
return value == uInt32.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Long.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/UInt64.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 无符号数整数(8位)
public class UInt64 {
public final static int bitWidth = 64;
public final static UInt64 min = new UInt64(0);
// 将max设置为有符号的long max,而不是无符号的long max,
// 因为Java无符号的long算术函数相对不成熟,因此很可能引起混乱。
public final static UInt64 max = new UInt64(Long.MAX_VALUE);
public final long value;
public UInt64(long value) {
this.value = (value < 0 ? 0 : (value > Long.MAX_VALUE ? Long.MAX_VALUE : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UInt64 uInt64 = (UInt64) o;
return value == uInt64.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Long.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/datatype/UInt8.java
================================================
package com.ABC.pioneer.sensor.datatype;
import java.util.Objects;
/// 无符号数整数(8位)
public class UInt8 {
public final static int bitWidth = 8;
public final static UInt8 min = new UInt8(0);
public final static UInt8 max = new UInt8(255);
public final int value;
public UInt8(int value) {
this.value = (value < 0 ? 0 : (value > 255 ? 255 : value));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UInt8 uInt8 = (UInt8) o;
return value == uInt8.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return Integer.toString(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/motion/ConcreteInertiaSensor.java
================================================
package com.ABC.pioneer.sensor.motion;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import com.ABC.pioneer.sensor.SensorDelegate;
import com.ABC.pioneer.sensor.datatype.InertiaLocationReference;
import com.ABC.pioneer.sensor.datatype.Location;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcreteInertiaSensor implements InertiaSensor {
private final Queue delegates = new ConcurrentLinkedQueue<>();
private final ExecutorService operationQueue = Executors.newSingleThreadExecutor();
private final Context context;
private final SensorManager sensorManager;
private final Sensor hardwareSensor;
private final SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
return;
}
try {
final Date timestamp = new Date();
final double x = event.values[0];
final double y = event.values[1];
final double z = event.values[2];
final InertiaLocationReference inertiaLocationReference = new InertiaLocationReference(x, y, z);
final Location didVisit = new Location(inertiaLocationReference, timestamp, timestamp);
} catch (Throwable e) {
return;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};
public ConcreteInertiaSensor(final Context context) {
this.context = context;
this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
this.hardwareSensor = (sensorManager == null ? null : sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER));
}
@Override
public void add(SensorDelegate delegate) {
delegates.add(delegate);
}
@Override
public void start() {
// 获取传感器管理器
if (sensorManager == null) {
return;
}
// 获取硬件传感器
if (hardwareSensor == null) {
return;
}
// 注册监听器
sensorManager.unregisterListener(sensorEventListener);
sensorManager.registerListener(sensorEventListener, hardwareSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
@Override
public void stop() {
if (sensorManager == null) {
return;
}
// 取消注册监听器
sensorManager.unregisterListener(sensorEventListener);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/motion/InertiaSensor.java
================================================
package com.ABC.pioneer.sensor.motion;
import com.ABC.pioneer.sensor.Sensor;
public interface InertiaSensor extends Sensor {
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/BasicFunc.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.datatype.Data;
/// 基本功能
public class BasicFunc {
/// hash函数SHA256
public static Data h(Data data) {
try {
byte[] hash = new byte[32];
PioneerHash ph = new PioneerHash();
ph.generateHash(data.value,hash);
return new Data(hash);
} catch (Throwable e) {
return null;
}
}
/// 截取函数 : 删除数据的后半部分
public static Data t(Data data) {
return t(data, data.value.length / 2);
}
/// 截取函数:保留数据的前n个字节
public static Data t(Data data, int n) {
return data.subdata(0, n);
}
/// 异或函数 : 计算左半部分异或右半部分,假设左和右的长度相同
public static Data xor(Data left, Data right) {
final byte[] leftByteArray = left.value;
final byte[] rightByteArray = right.value;
final byte[] resultByteArray = new byte[left.value.length];
for (int i=0; i
* Internal access to the digest is synchronized so a single one of these can be shared.
*
*/
public class DigestRandomGenerator
implements RandomGenerator
{
private byte[] state;
private byte[] seed;
private Digest digest;
private long stateCounter;
private long seedCounter;
private static long CYCLE_COUNT = 10;
private void cycleSeed()
{
digestUpdate(seed);
digestAddCounter(seedCounter++);
digestDoFinal(seed);
}
private void generateState()
{
digestAddCounter(stateCounter++);
digestUpdate(state);
digestUpdate(seed);
digestDoFinal(state);
if ((stateCounter % CYCLE_COUNT) == 0)
{
cycleSeed();
}
}
private void digestAddCounter(long seed)
{
for (int i = 0; i != 8; i++)
{
digest.update((byte)seed);
seed >>>= 8;
}
}
private void digestUpdate(byte[] inSeed)
{
digest.update(inSeed, 0, inSeed.length);
}
private void digestDoFinal(byte[] result)
{
digest.doFinal(result, 0);
}
// public constructors
public void addSeedMaterial(byte[] inSeed)
{
synchronized (this)
{
if (inSeed.length != 0)
{
digestUpdate(inSeed);
}
digestUpdate(seed);
digestDoFinal(seed);
}
}
public void addSeedMaterial(long rSeed)
{
synchronized (this)
{
digestAddCounter(rSeed);
digestUpdate(seed);
digestDoFinal(seed);
}
}
public DigestRandomGenerator(
Digest digest)
{
this.digest = digest;
this.seed = new byte[digest.getDigestSize()];
this.seedCounter = 1;
this.state = new byte[digest.getDigestSize()];
this.stateCounter = 1;
}
public void nextBytes(byte[] bytes)
{
nextBytes(bytes, 0, bytes.length);
}
public void nextBytes(byte[] bytes, int start, int len)
{
synchronized (this)
{
int stateOff = 0;
generateState();
int end = start + len;
for (int i = start; i != end; i++)
{
if (stateOff == state.length)
{
generateState();
stateOff = 0;
}
bytes[i] = state[stateOff++];
}
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/ExtendedDigest.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public interface ExtendedDigest
extends Digest
{
/**
* Return the size in bytes of the internal buffer the digest applies it's compression
* function to.
*
* @return byte length of the digests internal buffer.
*/
public int getByteLength();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/GeneralDigest.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public abstract class GeneralDigest implements ExtendedDigest,Memoable{
private static final int BYTE_LENGTH = 64;
private final byte[] xBuf = new byte[4];
private int xBufOff;
private long byteCount;
protected GeneralDigest()
{
xBufOff=0;
}
protected GeneralDigest(GeneralDigest t)
{
copyIn(t);
}
protected GeneralDigest(byte[] encodedState)
{
System.arraycopy(encodedState, 0, xBuf, 0, xBuf.length);
xBufOff = Util.bigEndianToInt(encodedState, 4);
byteCount = Util.bigEndianToLong(encodedState, 8);
}
protected void copyIn(GeneralDigest t)
{
System.arraycopy(t.xBuf, 0, xBuf, 0, t.xBuf.length);
xBufOff = t.xBufOff;
byteCount = t.byteCount;
}
public void update(byte in)
{
xBuf[xBufOff++] = in;
if (xBufOff == xBuf.length)
{
processWord(xBuf, 0);
xBufOff = 0;
}
byteCount++;
}
public void update(
byte[] in,
int inOff,
int len)
{
len = Math.max(0, len);
int i = 0;
if (xBufOff != 0)
{
while (i < len)
{
xBuf[xBufOff++] = in[inOff + i++];
if (xBufOff == 4)
{
processWord(xBuf, 0);
xBufOff = 0;
break;
}
}
}
// process whole words.
int limit = ((len - i) & ~3) + i;
for (; i < limit; i += 4)
{
processWord(in, inOff + i);
}
while (i < len)
{
xBuf[xBufOff++] = in[inOff + i++];
}
byteCount += len;
}
public void finish()
{
long bitLength = (byteCount << 3);
update((byte)128);
while (xBufOff != 0)
{
update((byte)0);
}
processLength(bitLength);
processBlock();
}
public void reset()
{
byteCount = 0;
xBufOff = 0;
for (int i = 0; i < xBuf.length; i++)
{
xBuf[i] = 0;
}
}
protected void populateState(byte[] state)
{
System.arraycopy(xBuf, 0, state, 0, xBufOff);
Util.intToBigEndian(xBufOff, state, 4);
Util.longToBigEndian(byteCount, state, 8);
}
public int getByteLength()
{
return BYTE_LENGTH;
}
protected abstract void processWord(byte[] in, int inOff);
protected abstract void processLength(long bitLength);
protected abstract void processBlock();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/GenerateKey.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import java.text.SimpleDateFormat;
import java.util.Date;
/// 生成相应的keys,链式生成
public class GenerateKey {
/// Secret key的长度设定为2048
private final static int secretKeyLength = 2048;
/// 派生keys所能维持到的最大天数为2000天
private final static int days = 2000;
/// 生成matching keys的时间间隔为6min
private final static int periods = 240;
/// 获取1970年以来的时间间隔
private final static TimeInterval epoch = GenerateKey.getEpoch();
///转换时间显示为相应的格式
protected static Date date(String fromString) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
try {
return format.parse(fromString);
} catch (Throwable e) {
return null;
}
}
/// 计算用到的是具体的那天或哪个间隔的key
protected static TimeInterval getEpoch() {
final Date date = date("2020-09-01T00:00:00+0000");
return new TimeInterval(date.getTime() / 1000);
}
/// 选择正确的天数来获取相应的matching key
public static int day(Date onDate) {
return (int) ((new TimeInterval(onDate).value - epoch.value) / 86400);
}
/// 获取每天相应正确的时间间隔来获取相应的contact key
protected static int period(Date atTime) {
final int second = (int) ((new TimeInterval(atTime).value - epoch.value) % 86400);
return second / (86400 / periods);
}
/// 生成secret key, K_s
public static SecretKey secretKey() {
//用SM3生成随机数
PioneerPRG prg = new PioneerPRG();
final byte[] bytes = new byte[secretKeyLength];
prg.generateRandomNumber(bytes);
return new SecretKey(bytes);
}
/// 生成 matching keys
public static MatchingKey[] matchingKeys(SecretKey secretKey) {
final int n = days;
//这里的密钥是由hash链式生成的,以确保密钥不能反向生成前向密钥
final MatchingKeySeed[] matchingKeySeed = new MatchingKeySeed[n + 1];
//当最后一个Matching key耗尽的时候,生成新的密钥
matchingKeySeed[n] = new MatchingKeySeed(BasicFunc.h(secretKey));
for (int i=n; i-->0;) {
matchingKeySeed[i] = new MatchingKeySeed(BasicFunc.h(BasicFunc.t(matchingKeySeed[i + 1])));
}
//第i天的匹配密钥是第i天或第i-1天的匹配密钥种子的哈希
final MatchingKey[] matchingKey = new MatchingKey[n + 1];
for (int i=1; i<=n; i++) {
matchingKey[i] = new MatchingKey(BasicFunc.h(BasicFunc.xor(matchingKeySeed[i], matchingKeySeed[i - 1])));
}
// 第0天的匹配密钥来自第0天和第-1天的匹配密钥种子。
// 在上面的代码中为清楚起见我们将其列为特殊情况。
final MatchingKeySeed matchingKeySeedMinusOne = new MatchingKeySeed(BasicFunc.h(BasicFunc.t(matchingKeySeed[0])));
matchingKey[0] = new MatchingKey(BasicFunc.h(BasicFunc.xor(matchingKeySeed[0], matchingKeySeedMinusOne)));
return matchingKey;
}
/// 生成 contact keys
public static ContactKey[] contactKeys(MatchingKey matchingKey) {
final int n = periods;
final ContactKeySeed[] contactKeySeed = new ContactKeySeed[n + 1];
//在第240个间隔(一天的最后6分钟),第i天的最后一个联系人密钥种子是第i天的匹配密钥的哈希。
contactKeySeed[n] = new ContactKeySeed(BasicFunc.h(matchingKey));
for (int j=n; j-->0;) {
contactKeySeed[j] = new ContactKeySeed(BasicFunc.h(BasicFunc.t(contactKeySeed[j + 1])));
}
final ContactKey[] contactKey = new ContactKey[n + 1];
for (int j=1; j<=n; j++) {
contactKey[j] = new ContactKey(BasicFunc.h(BasicFunc.xor(contactKeySeed[j], contactKeySeed[j - 1])));
}
final ContactKeySeed contactKeySeedMinusOne = new ContactKeySeed(BasicFunc.h(BasicFunc.t(contactKeySeed[0])));
contactKey[0] = new ContactKey(BasicFunc.h(BasicFunc.xor(contactKeySeed[0], contactKeySeedMinusOne)));
return contactKey;
}
/// 生成 contact identifer,截取contact keys的前16字节
public static ContactIdentifier contactIdentifier(ContactKey contactKey) {
return new ContactIdentifier(BasicFunc.t(contactKey, 16));
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/HMACSHA256.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import java.util.Arrays;
public class HMACSHA256 {
public static byte [] GenerateMAC(byte [] data, byte [] key) throws Exception
{
byte[] mac = new byte[32];
PioneerHMac pmac = new PioneerHMac(key);
pmac.generateHMac(data,mac);
return mac;
}
public static boolean VerifyMAC(byte [] data, byte [] key,byte [] Mac_str) throws Exception
{
byte [] Gen_Mac_str = null;
Gen_Mac_str = GenerateMAC(data,key);
if(Arrays.equals(Mac_str,Gen_Mac_str))
return true;
else
return false;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/HMac.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import java.util.Hashtable;
/**
* HMAC implementation based on RFC2104
*
* H(K XOR opad, H(K XOR ipad, text))
*/
public class HMac
implements Mac
{
private final static byte IPAD = (byte)0x36;
private final static byte OPAD = (byte)0x5C;
private Digest digest;
private int digestSize;
private int blockLength;
private Memoable ipadState;
private Memoable opadState;
private byte[] inputPad;
private byte[] outputBuf;
private static Hashtable blockLengths;
static
{
blockLengths = new Hashtable();
blockLengths.put("SM3", Integer.valueOf(64));
}
private static int getByteLength(
Digest digest)
{
if (digest instanceof ExtendedDigest)
{
return ((ExtendedDigest)digest).getByteLength();
}
Integer b = (Integer)blockLengths.get(digest.getAlgorithmName());
if (b == null)
{
throw new IllegalArgumentException("unknown digest passed: " + digest.getAlgorithmName());
}
return b.intValue();
}
/**
* Base constructor for one of the standard digest algorithms that the
* byteLength of the algorithm is know for.
*
* @param digest the digest.
*/
public HMac(
Digest digest)
{
this(digest, getByteLength(digest));
}
private HMac(
Digest digest,
int byteLength)
{
this.digest = digest;
this.digestSize = digest.getDigestSize();
this.blockLength = byteLength;
this.inputPad = new byte[blockLength];
this.outputBuf = new byte[blockLength + digestSize];
}
public String getAlgorithmName()
{
return digest.getAlgorithmName() + "/HMAC";
}
public Digest getUnderlyingDigest()
{
return digest;
}
public void init(
CipherParameters params)
{
digest.reset();
byte[] key = ((KeyParameter)params).getKey();
int keyLength = key.length;
if (keyLength > blockLength)
{
digest.update(key, 0, keyLength);
digest.doFinal(inputPad, 0);
keyLength = digestSize;
}
else
{
System.arraycopy(key, 0, inputPad, 0, keyLength);
}
for (int i = keyLength; i < inputPad.length; i++)
{
inputPad[i] = 0;
}
System.arraycopy(inputPad, 0, outputBuf, 0, blockLength);
xorPad(inputPad, blockLength, IPAD);
xorPad(outputBuf, blockLength, OPAD);
if (digest instanceof Memoable)
{
opadState = ((Memoable)digest).copy();
((Digest)opadState).update(outputBuf, 0, blockLength);
}
digest.update(inputPad, 0, inputPad.length);
if (digest instanceof Memoable)
{
ipadState = ((Memoable)digest).copy();
}
}
public int getMacSize()
{
return digestSize;
}
public void update(
byte in)
{
digest.update(in);
}
public void update(
byte[] in,
int inOff,
int len)
{
digest.update(in, inOff, len);
}
public int doFinal(
byte[] out,
int outOff)
{
digest.doFinal(outputBuf, blockLength);
if (opadState != null)
{
((Memoable)digest).reset(opadState);
digest.update(outputBuf, blockLength, digest.getDigestSize());
}
else
{
digest.update(outputBuf, 0, outputBuf.length);
}
int len = digest.doFinal(out, outOff);
for (int i = blockLength; i < outputBuf.length; i++)
{
outputBuf[i] = 0;
}
if (ipadState != null)
{
((Memoable)digest).reset(ipadState);
}
else
{
digest.update(inputPad, 0, inputPad.length);
}
return len;
}
/**
* Reset the mac generator.
*/
public void reset()
{
/*
* reset the underlying digest.
*/
digest.reset();
/*
* reinitialize the digest.
*/
digest.update(inputPad, 0, inputPad.length);
}
private static void xorPad(byte[] pad, int len, byte n)
{
for (int i = 0; i < len; ++i)
{
pad[i] ^= n;
}
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/KeyParameter.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public class KeyParameter
implements CipherParameters
{
private byte[] key;
public KeyParameter(
byte[] key)
{
this(key, 0, key.length);
}
public KeyParameter(
byte[] key,
int keyOff,
int keyLen)
{
this.key = new byte[keyLen];
System.arraycopy(key, keyOff, this.key, 0, keyLen);
}
public byte[] getKey()
{
return key;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/Mac.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
/**
* The base interface for implementations of message authentication codes (MACs).
*/
public interface Mac
{
/**
* Initialise the MAC.
*
* @param params the key and other data required by the MAC.
* @exception IllegalArgumentException if the params argument is
* inappropriate.
*/
public void init(CipherParameters params)
throws IllegalArgumentException;
/**
* Return the name of the algorithm the MAC implements.
*
* @return the name of the algorithm the MAC implements.
*/
public String getAlgorithmName();
/**
* Return the block size for this MAC (in bytes).
*
* @return the block size for this MAC in bytes.
*/
public int getMacSize();
/**
* add a single byte to the mac for processing.
*
* @param in the byte to be processed.
* @exception IllegalStateException if the MAC is not initialised.
*/
public void update(byte in);
/**
* @param in the array containing the input.
* @param inOff the index in the array the data begins at.
* @param len the length of the input starting at inOff.
*/
public void update(byte[] in, int inOff, int len);
/**
* Compute the final stage of the MAC writing the output to the out
* parameter.
*
* doFinal leaves the MAC in the same state it was after the last init.
*
* @param out the array the MAC is to be output to.
* @param outOff the offset into the out buffer the output is to start at.
*/
public int doFinal(byte[] out, int outOff);
/**
* Reset the MAC. At the end of resetting the MAC should be in the
* in the same state it was after the last init (if there was one).
*/
public void reset();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/MatchingKey.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.datatype.Data;
/// Matching key
public class MatchingKey extends Data {
public MatchingKey(Data value) {
super(value);
}
public MatchingKey(byte repeating, int count) {
super(repeating, count);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/MatchingKeySeed.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.datatype.Data;
/// Matching key种子
public class MatchingKeySeed extends Data {
public MatchingKeySeed(Data value) {
super(value);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/Memoable.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public interface Memoable
{
/**
* copy this Memoable object to generate a new Memoable object
* @return the new Memoable object
*/
Memoable copy();
/**
* reset this Memoable object according to the other Memoable object's state
* @param other the provided Memoable object
*/
void reset(Memoable other);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/PioneerHMac.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public class PioneerHMac {
Digest digest;
Mac hmac;
public PioneerHMac(byte[] key)
{
digest = new SM3Digest();
hmac = new HMac(digest);
hmac.init(new KeyParameter(key));
}
public void generateHMac(byte[] message,byte[] mac)
{
generateHMac(message,0,message.length,mac,0);
}
public void generateHMac(byte[] message,int inoff,int len,byte[] mac,int outoff)
{
hmac.update(message,inoff,len);
hmac.doFinal(mac,outoff);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/PioneerHash.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public class PioneerHash {
Digest digest;
public PioneerHash()
{
digest = new SM3Digest();
}
public void generateHash(byte[] message,byte[] hash)
{
generateHash(message,0,message.length,hash,0);
}
public void generateHash(byte[] message,int inoff,int len,byte[] hash,int outoff)
{
digest.update(message,inoff,len);
digest.doFinal(hash,outoff);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/PioneerPRG.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import java.security.SecureRandom;
public class PioneerPRG {
Digest digest;
RandomGenerator prg;
public PioneerPRG()
{
digest = new SM3Digest();
prg = new DigestRandomGenerator(digest);
}
public void generateRandomNumber(byte[] bytes)
{
generateRandomNumber(bytes,0,bytes.length);
}
public void generateRandomNumber(byte[] bytes, int start, int len)
{
final SecureRandom seedSr = new SecureRandom();
final byte[] seed = seedSr.generateSeed(55);
prg.addSeedMaterial(seed);
prg.nextBytes(bytes,start,len);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/RandomGenerator.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
/**
* Generic interface for objects generating random bytes.
*/
public interface RandomGenerator
{
/**
* Add more seed material to the generator.
*
* @param seed a byte array to be mixed into the generator's state.
*/
void addSeedMaterial(byte[] seed);
/**
* Add more seed material to the generator.
*
* @param seed a long value to be mixed into the generator's state.
*/
void addSeedMaterial(long seed);
/**
* Fill bytes with random values.
*
* @param bytes byte array to be filled.
*/
void nextBytes(byte[] bytes);
/**
* Fill part of bytes with random values.
*
* @param bytes byte array to be filled.
* @param start index to start filling at.
* @param len length of segment to fill.
*/
void nextBytes(byte[] bytes, int start, int len);
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/SM3Digest.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
public class SM3Digest
extends GeneralDigest
{
private static final int DIGEST_LENGTH = 32; // bytes
private int[] V = new int[DIGEST_LENGTH / 4]; // in 32 bit ints (8 ints)
private int[] inwords = new int[BLOCK_SIZE];
private static final int BLOCK_SIZE = 64 / 4; // of 32 bit ints (16 ints)
private int xOff;
// Round constant T for processBlock() which is 32 bit integer rolled left up to (63 MOD 32) bit positions.
private static final int[] T = new int[64];
// Work-bufs used within processBlock()
private int[] W = new int[68];
static
{
for (int i = 0; i < 16; ++i)
{
int t = 0x79CC4519;
T[i] = (t << i) | (t >>> (32 - i));
}
for (int i = 16; i < 64; ++i)
{
int n = i % 32;
int t = 0x7A879D8A;
T[i] = (t << n) | (t >>> (32 - n));
}
}
/**
* Copy constructor. This will copy the state of the provided
* message digest.
*/
public SM3Digest(SM3Digest t)
{
super(t);
copyIn(t);
}
/**
* Standard constructor
*/
public SM3Digest()
{
reset();
}
private void copyIn(SM3Digest t)
{
System.arraycopy(t.V, 0, this.V, 0, this.V.length);
System.arraycopy(t.inwords, 0, this.inwords, 0, this.inwords.length);
xOff = t.xOff;
}
public String getAlgorithmName()
{
return "SM3";
}
public int getDigestSize()
{
return DIGEST_LENGTH;
}
public void reset(Memoable other)
{
SM3Digest d = (SM3Digest)other;
super.copyIn(d);
copyIn(d);
}
public Memoable copy()
{
return new SM3Digest(this);
}
/**
* reset the chaining variables
*/
public void reset()
{
super.reset();
this.V[0] = 0x7380166F;
this.V[1] = 0x4914B2B9;
this.V[2] = 0x172442D7;
this.V[3] = 0xDA8A0600;
this.V[4] = 0xA96F30BC;
this.V[5] = 0x163138AA;
this.V[6] = 0xE38DEE4D;
this.V[7] = 0xB0FB0E4E;
this.xOff = 0;
}
public int doFinal(byte[] out,
int outOff)
{
finish();
Util.intToBigEndian(V, out, outOff);
reset();
return DIGEST_LENGTH;
}
protected void processLength(long bitLength)
{
if (this.xOff > (BLOCK_SIZE - 2))
{
// xOff == 15 --> can't fit the 64 bit length field at tail..
this.inwords[this.xOff] = 0; // fill with zero
++this.xOff;
processBlock();
}
// Fill with zero words, until reach 2nd to last slot
while (this.xOff < (BLOCK_SIZE - 2))
{
this.inwords[this.xOff] = 0;
++this.xOff;
}
// Store input data length in BITS
this.inwords[this.xOff++] = (int)(bitLength >>> 32);
this.inwords[this.xOff++] = (int)(bitLength);
}
protected void processWord(byte[] in,
int inOff)
{
// Note: Inlined for performance
// this.inwords[xOff] = Util.bigEndianToInt(in, inOff);
int n = (((in[inOff] & 0xff) << 24) |
((in[++inOff] & 0xff) << 16) |
((in[++inOff] & 0xff) << 8) |
((in[++inOff] & 0xff)));
this.inwords[this.xOff] = n;
++this.xOff;
if (this.xOff >= 16)
{
processBlock();
}
}
private int P0(final int x)
{
final int r9 = ((x << 9) | (x >>> (32 - 9)));
final int r17 = ((x << 17) | (x >>> (32 - 17)));
return (x ^ r9 ^ r17);
}
private int P1(final int x)
{
final int r15 = ((x << 15) | (x >>> (32 - 15)));
final int r23 = ((x << 23) | (x >>> (32 - 23)));
return (x ^ r15 ^ r23);
}
private int FF0(final int x, final int y, final int z)
{
return (x ^ y ^ z);
}
private int FF1(final int x, final int y, final int z)
{
return ((x & y) | (x & z) | (y & z));
}
private int GG0(final int x, final int y, final int z)
{
return (x ^ y ^ z);
}
private int GG1(final int x, final int y, final int z)
{
return ((x & y) | ((~x) & z));
}
protected void processBlock()
{
for (int j = 0; j < 16; ++j)
{
this.W[j] = this.inwords[j];
}
for (int j = 16; j < 68; ++j)
{
int wj3 = this.W[j - 3];
int r15 = ((wj3 << 15) | (wj3 >>> (32 - 15)));
int wj13 = this.W[j - 13];
int r7 = ((wj13 << 7) | (wj13 >>> (32 - 7)));
this.W[j] = P1(this.W[j - 16] ^ this.W[j - 9] ^ r15) ^ r7 ^ this.W[j - 6];
}
int A = this.V[0];
int B = this.V[1];
int C = this.V[2];
int D = this.V[3];
int E = this.V[4];
int F = this.V[5];
int G = this.V[6];
int H = this.V[7];
for (int j = 0; j < 16; ++j)
{
int a12 = ((A << 12) | (A >>> (32 - 12)));
int s1_ = a12 + E + T[j];
int SS1 = ((s1_ << 7) | (s1_ >>> (32 - 7)));
int SS2 = SS1 ^ a12;
int Wj = W[j];
int W1j = Wj ^ W[j + 4];
int TT1 = FF0(A, B, C) + D + SS2 + W1j;
int TT2 = GG0(E, F, G) + H + SS1 + Wj;
D = C;
C = ((B << 9) | (B >>> (32 - 9)));
B = A;
A = TT1;
H = G;
G = ((F << 19) | (F >>> (32 - 19)));
F = E;
E = P0(TT2);
}
// Different FF,GG functions on rounds 16..63
for (int j = 16; j < 64; ++j)
{
int a12 = ((A << 12) | (A >>> (32 - 12)));
int s1_ = a12 + E + T[j];
int SS1 = ((s1_ << 7) | (s1_ >>> (32 - 7)));
int SS2 = SS1 ^ a12;
int Wj = W[j];
int W1j = Wj ^ W[j + 4];
int TT1 = FF1(A, B, C) + D + SS2 + W1j;
int TT2 = GG1(E, F, G) + H + SS1 + Wj;
D = C;
C = ((B << 9) | (B >>> (32 - 9)));
B = A;
A = TT1;
H = G;
G = ((F << 19) | (F >>> (32 - 19)));
F = E;
E = P0(TT2);
}
this.V[0] ^= A;
this.V[1] ^= B;
this.V[2] ^= C;
this.V[3] ^= D;
this.V[4] ^= E;
this.V[5] ^= F;
this.V[6] ^= G;
this.V[7] ^= H;
this.xOff = 0;
}
}
/*
3.4.2. Constants
Tj = 79cc4519 when 0 < = j < = 15
Tj = 7a879d8a when 16 < = j < = 63
3.4.3. Boolean function
FFj(X;Y;Z) = X XOR Y XOR Z when 0 < = j < = 15
= (X AND Y) OR (X AND Z) OR (Y AND Z) when 16 < = j < = 63
GGj(X;Y;Z) = X XOR Y XOR Z when 0 < = j < = 15
= (X AND Y) OR (NOT X AND Z) when 16 < = j < = 63
The X, Y, Z in the fomular are words!GBP
3.4.4. Permutation function
P0(X) = X XOR (X <<< 9) XOR (X <<< 17) ## ROLL, not SHIFT
P1(X) = X XOR (X <<< 15) XOR (X <<< 23) ## ROLL, not SHIFT
The X in the fomular are a word.
----------
Each ROLL converted to Java expression:
ROLL 9 : ((x << 9) | (x >>> (32-9))))
ROLL 17 : ((x << 17) | (x >>> (32-17)))
ROLL 15 : ((x << 15) | (x >>> (32-15)))
ROLL 23 : ((x << 23) | (x >>> (32-23)))
*/
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/SecretKey.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.datatype.Data;
/// Secret key
public class SecretKey extends Data {
public SecretKey(byte[] value) {
super(value);
}
public SecretKey(byte repeating, int count) {
super(repeating, count);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/Crypto/SpecificUsePayloadSupplier.java
================================================
package com.ABC.pioneer.sensor.payload.Crypto;
import com.ABC.pioneer.sensor.Device;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadTimestamp;
import com.ABC.pioneer.sensor.payload.DefaultPayloadSupplier;
import com.ABC.pioneer.sensor.datatype.UInt64;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/// 简单的有效载荷标识
public class SpecificUsePayloadSupplier extends DefaultPayloadSupplier implements UsePayloadSupplier {
// 缓存当天的联系人标识符
private static Integer day = null;
private static ContactIdentifier[] contactIdentifiers = null;
//定义固定的5字节首部
private static final Data commonPayload = new Data((byte)0,5);
public static MatchingKey[] matchingKeys;
public final static int payloadLength = 61;
public SpecificUsePayloadSupplier(SecretKey secretKey) {
// 所有数据均为大端
// 从秘密密钥生成匹配密钥
matchingKeys = GenerateKey.matchingKeys(secretKey);
}
// 生成一个新的密钥
public static SecretKey generateSecretKey() {
return GenerateKey.secretKey();
}
public MatchingKey matchingkey()
{
return matchingKeys[day];
}
//解析Mac
public static Data parseMac(PayloadData payloadData) {
return payloadData.subdata(29,32);
}
//解析ontactIdentifier
public static Data parseContactIdentifier(PayloadData payloadData) {
return payloadData.subdata(5,16);
}
//解析StartTime
public static Data parseStartTime(PayloadData payloadData) {
return payloadData.subdata(21,8);
}
// 由matching key生成contactidentifiers
public static ContactIdentifier[] contactIdentifiers(MatchingKey matchingKey) {
final ContactKey[] contactKeys = GenerateKey.contactKeys(matchingKey);
final ContactIdentifier[] contactIdentifiers = new ContactIdentifier[contactKeys.length];
for (int i=contactKeys.length; i-->0;) {
contactIdentifiers[i] = GenerateKey.contactIdentifier(contactKeys[i]);
}
return contactIdentifiers;
}
public static Data parseRawData(PayloadData payloadData) {
return payloadData.subdata(5,24);
}
private static void appendTimeandMac(Date start_time, PayloadData payloadData) {
final long start;
final long Start;
start = start_time.getTime()/1000;
Start = start*1000;
// 有效开始时间和结束时间
try {
payloadData.append(new UInt64(Start));
}
catch(Exception e)
{
payloadData.append(new Data((byte)0,8));
}
// 添加 MAC
if (day == null || matchingKeys == null)
payloadData.append(new Data((byte)0,32));
if (!(day >= 0 && day < matchingKeys.length)) {
payloadData.append(new Data((byte)0,32));
}
Data Rowdata = SpecificUsePayloadSupplier.parseRawData(payloadData);
try {
Data mac = new Data(HMACSHA256.GenerateMAC(Rowdata.value, matchingKeys[day].value));
payloadData.append(mac);
} catch (Exception e) {
payloadData.append(new Data((byte)0,32));
}
}
// SimplePayloadDataSupplier
//用来更新UI界面Payload的函数
public static PayloadData updatePayload(Date time){
final int day = GenerateKey.day(time);
final int period = GenerateKey.period(time);
final PayloadData payloadData = new PayloadData();
payloadData.append(commonPayload);
// 添加contactIdentifier
final ContactIdentifier contactIdentifier = contactIdentifiers(matchingKeys[day])[period];
if (contactIdentifier != null) {
payloadData.append(contactIdentifier);
} else {
payloadData.append(new ContactIdentifier((byte) 0, 16));
}
return payloadData;
}
@Override
public PayloadData payload(PayloadTimestamp timestamp, Device device) {
final PayloadData payloadData = new PayloadData();
payloadData.append(commonPayload);
// 添加contactIdentifier
final ContactIdentifier contactIdentifier = contactIdentifier(timestamp.value);
if (contactIdentifier != null) {
payloadData.append(contactIdentifier);
} else {
payloadData.append(new ContactIdentifier((byte) 0, 16));
}
// 添加开始时间和结束时间以及MAC
appendTimeandMac(timestamp.value, payloadData);
return payloadData;
}
// 根据时间time生成当前时间的contactidentifier
private ContactIdentifier contactIdentifier(Date time) {
final int day = GenerateKey.day(time);
final int period = GenerateKey.period(time);
if (!(day >= 0 && day < matchingKeys.length)) {
return null;
}
// Generate and cache contact keys for specific day on-demand
if (this.day == null || this.day != day) {
contactIdentifiers = contactIdentifiers(matchingKeys[day]);
this.day = day;
}
if (contactIdentifiers == null) {
return null;
}
if (!(period >= 0 && period < contactIdentifiers.length)) {
return null;
}
// 安全性检验
if (contactIdentifiers[period].value.length != 16) {
return null;
}
return contactIdentifiers[period];
}
@Override
public List payload(Data data) {
// 将包含级联有效载荷的原始数据拆分为单个有效载荷
final List payloads = new ArrayList<>();
final byte[] bytes = data.value;
for (int index = 0; (index + payloadLength) <= bytes.length; index += payloadLength) {
final byte[] payloadBytes = new byte[payloadLength];
System.arraycopy(bytes, index, payloadBytes, 0, payloadLength);
payloads.add(new PayloadData(payloadBytes));
}
return payloads;
}
// 检查payload时间
public static boolean checkPayloadtime(PayloadData payloadData) {
try {
Data timeData = parseStartTime(payloadData);
final long s = timeData.uint64(0).value;
final long e = s + 360000;
final long r = new Date().getTime();
if (r > s && r < e){
return true;
}
else{
return false;
}
}
catch(Exception e)
{
return false;
}
}
//解析StartTime
static public long parseStartTimeToLong(PayloadData payloadData) {
final long s = timeData.uint64(0).value;
Data timeData = parseStartTime(payloadData);
return s;
}
//解析ContactIdentifier
static public String parseContactIdentifierToStr(PayloadData payloadData){
final byte[] parsedata = new byte[16];
String Step;
System.arraycopy(payloadData.value,5,parsedata,0,16);
StringBuffer result = new StringBuffer(parsedata.length);
for(int j=0;j>> 24);
bs[++off] = (byte)(n >>> 16);
bs[++off] = (byte)(n >>> 8);
bs[++off] = (byte)(n );
}
public static void intToBigEndian(int[] ns, byte[] bs, int off)
{
for (int i = 0; i < ns.length; ++i)
{
intToBigEndian(ns[i], bs, off);
off += 4;
}
}
public static long bigEndianToLong(byte[] bs, int off)
{
int hi = bigEndianToInt(bs, off);
int lo = bigEndianToInt(bs, off + 4);
return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL);
}
public static void longToBigEndian(long n, byte[] bs, int off)
{
intToBigEndian((int)(n >>> 32), bs, off);
intToBigEndian((int)(n & 0xffffffffL), bs, off + 4);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/DefaultPayloadSupplier.java
================================================
package com.ABC.pioneer.sensor.payload;
import com.ABC.pioneer.sensor.Device;
import com.ABC.pioneer.sensor.PayloadSupplier;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.LegacyPayload;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.PayloadTimestamp;
import java.util.ArrayList;
import java.util.List;
// 默认的有效载荷数据提供者,实现了固定长度的有效载荷拆分方法。
public abstract class DefaultPayloadSupplier implements PayloadSupplier {
@Override
public LegacyPayload legacyPayload(PayloadTimestamp timestamp, Device device) {
return null;
}
@Override
public List payload(Data data) {
// 获取固定长度的有效载荷数据
final PayloadData fixedLengthPayloadData = payload(new PayloadTimestamp(), null);
final int payloadDataLength = fixedLengthPayloadData.value.length;
// 将包含串联有效负载的原始数据拆分为单独的有效负载
final List payloads = new ArrayList<>();
final byte[] bytes = data.value;
for (int index = 0; (index + payloadDataLength) <= bytes.length; index += payloadDataLength) {
final byte[] payloadBytes = new byte[payloadDataLength];
System.arraycopy(bytes, index, payloadBytes, 0, payloadDataLength);
payloads.add(new PayloadData(payloadBytes));
}
return payloads;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/DigitalSignature.java
================================================
package com.ABC.pioneer.sensor.payload;
import com.ABC.pioneer.sensor.datatype.Data;
public class DigitalSignature extends Data {
private static Data message = new Data();
public DigitalSignature(Data message){this.message = message;}
public DigitalSignature(){this.message = message;}
public static Data genMAC(){return message;}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/extended/ConcreteExtendedDataSectionV1.java
================================================
package com.ABC.pioneer.sensor.payload.extended;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.UInt8;
public class ConcreteExtendedDataSectionV1 {
public final UInt8 code;
public final UInt8 length;
public final Data data;
public ConcreteExtendedDataSectionV1(UInt8 code, UInt8 length, Data data) {
this.code = code;
this.length = length;
this.data = data;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/extended/ConcreteExtendedDataV1.java
================================================
package com.ABC.pioneer.sensor.payload.extended;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.Float16;
import com.ABC.pioneer.sensor.datatype.Int16;
import com.ABC.pioneer.sensor.datatype.Int32;
import com.ABC.pioneer.sensor.datatype.Int64;
import com.ABC.pioneer.sensor.datatype.Int8;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.UInt16;
import com.ABC.pioneer.sensor.datatype.UInt32;
import com.ABC.pioneer.sensor.datatype.UInt64;
import com.ABC.pioneer.sensor.datatype.UInt8;
import java.util.ArrayList;
import java.util.List;
public class ConcreteExtendedDataV1 implements ExtendedData {
private final PayloadData payloadData;
public ConcreteExtendedDataV1() {
payloadData = new PayloadData();
}
public ConcreteExtendedDataV1(PayloadData unparsedData) {
payloadData = unparsedData;
}
@Override
public boolean hasData() {
return 0 != payloadData.value.length;
}
@Override
public void addSection(UInt8 code, UInt8 value) {
payloadData.append(code);
payloadData.append(new UInt8(1));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, UInt16 value) {
payloadData.append(code);
payloadData.append(new UInt8(2));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, UInt32 value) {
payloadData.append(code);
payloadData.append(new UInt8(4));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, UInt64 value) {
payloadData.append(code);
payloadData.append(new UInt8(8));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Int8 value) {
payloadData.append(code);
payloadData.append(new UInt8(1));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Int16 value) {
payloadData.append(code);
payloadData.append(new UInt8(2));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Int32 value) {
payloadData.append(code);
payloadData.append(new UInt8(4));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Int64 value) {
payloadData.append(code);
payloadData.append(new UInt8(8));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Float16 value) {
payloadData.append(code);
payloadData.append(new UInt8(2));
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, String value) {
payloadData.append(code);
// Append String adds length to payload
payloadData.append(value);
}
@Override
public void addSection(UInt8 code, Data value) {
payloadData.append(code);
payloadData.append(new UInt8(value.value.length));
payloadData.append(value);
}
@Override
public PayloadData payload() {
return payloadData;
}
public List getSections() {
final List sections = new ArrayList<>();
int pos = 0;
while (pos < payloadData.value.length) {
if (payloadData.value.length - 2 <= pos) { // at least 3 in length
pos = payloadData.value.length;
continue;
}
// read code
UInt8 code = payloadData.uint8(pos);
pos = pos + 1;
// read length
UInt8 length = payloadData.uint8(pos);
pos = pos + 1;
// sanity check length
if (pos + length.value > payloadData.value.length) {
length = new UInt8(payloadData.value.length - pos);
}
// extract data
Data data = payloadData.subdata(pos, length.value);
sections.add(new ConcreteExtendedDataSectionV1(code, length, data));
// repeat
pos = pos + length.value;
}
return sections;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/payload/extended/ExtendedData.java
================================================
package com.ABC.pioneer.sensor.payload.extended;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.Float16;
import com.ABC.pioneer.sensor.datatype.Int16;
import com.ABC.pioneer.sensor.datatype.Int32;
import com.ABC.pioneer.sensor.datatype.Int64;
import com.ABC.pioneer.sensor.datatype.Int8;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.UInt16;
import com.ABC.pioneer.sensor.datatype.UInt32;
import com.ABC.pioneer.sensor.datatype.UInt64;
import com.ABC.pioneer.sensor.datatype.UInt8;
public interface ExtendedData {
boolean hasData();
void addSection(UInt8 code, UInt8 value);
void addSection(UInt8 code, UInt16 value);
void addSection(UInt8 code, UInt32 value);
void addSection(UInt8 code, UInt64 value);
void addSection(UInt8 code, Int8 value);
void addSection(UInt8 code, Int16 value);
void addSection(UInt8 code, Int32 value);
void addSection(UInt8 code, Int64 value);
void addSection(UInt8 code, Float16 value);
void addSection(UInt8 code, String value);
void addSection(UInt8 code, Data value);
PayloadData payload();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/AlarmReceiver.java
================================================
package com.ABC.pioneer.sensor.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.payload.Crypto.GenerateKey;
import com.ABC.pioneer.sensor.payload.Crypto.MatchingKey;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static android.content.Context.MODE_PRIVATE;
public class AlarmReceiver {
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private final Context context;
private String Maching_keys="";
private final MatchDelegate delegate;
private final Runnable UpdateDatabase = new Runnable() {
@Override
public void run() {
Log.d("BackService", new Date().toString());
PioneerDb db = new PioneerDb(context,"payloads",null,1);
db.updateTable();
}
};
private final Runnable DownloadData = new Runnable() {
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void run() {
// 此处添加下载的代码
// 下载之后进行匹配
// 有一个函数,下载数据并进行匹配,返回 true或 flase
// 根据true或者false,决定是否调用上传的函数
int date = GenerateKey.day(new Date());
// 用执行下载匹配算法时的时间减去两个小时的时间再减去开启app当天零点的时间,然后算出距离开启app过去了多少天,
// 用天数模14再加一找出哪个表是上一天的表,通过这个表向前找就能找将每张表都对应到相应的日期
try {
Connection connection = new Connection(context);
connection.ConnectToServer();
String str = connection.Download_Message();
int i = 0, k = 0;
int index1 = 0;
int index2 = 0;
String temp = "";
MatchingKey K;
int tag = 0;
while (true) {
if (tag == 1) break;
index1 = str.indexOf("ABC", i);
if (index1 == -1) break;
index1 = i + 3;
index2 = str.indexOf("ABC", index1 + 44);
if (index2 == -1) break;
temp = str.substring(index1, index2);
int num_of_Maching_key = temp.length() / 44;
int j = 0;
String temp_key;
for (int m = 0; m < num_of_Maching_key; m++) {
//由tmep_key生成那一天的240个contaceed_key
//再生成240个identifier
//由这240个identifier去匹配那一天的数据表
//找到匹配到的,再去判断mac
//验证通过就break,上传14天
//否则继续匹配
temp_key = temp.substring(j, j + 44);
Data data = new Data(temp_key);
K = new MatchingKey(data);
//打开数据库
PioneerDb db = new PioneerDb(context, "payloads", null, 1);
if (db.matchMatchingKey(K, 13 - m)) {
// 通知代理
delegate.matchFound();
SharedPreferences sp = context.getSharedPreferences("PhoneNumber", MODE_PRIVATE);
String phone = sp.getString("PhoneNumber", "默认值");
MatchingKey[] MachingKeys = SpecificUsePayloadSupplier.matchingKeys;
for (int a = 13; a >= 0; a--) {
Maching_keys += MachingKeys[date - a].base64EncodedString();
}
String result = connection.TransmitMachingKeys(phone, Maching_keys);
//Toast.makeText(TokenActivity.this, "成功上传ID和14天Maching_keys", Toast.LENGTH_LONG).show();
tag = 1;
break;
}
j += 44;
}
i = index2;
}
} catch(IOException e){
e.printStackTrace();
}
}
};
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(CustomTimer.UpdateDatabaseAction.equals(intent.getAction()))
executorService.execute(UpdateDatabase);
else if(CustomTimer.DownloadDataAction.equals(intent.getAction()))
executorService.execute(DownloadData);
else
;
}
};
public AlarmReceiver(Context context,MatchDelegate delegate)
{
this.delegate = delegate;
this.context = context;
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CustomTimer.DownloadDataAction);
intentFilter.addAction(CustomTimer.UpdateDatabaseAction);
context.registerReceiver(broadcastReceiver, intentFilter);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/Connection.java
================================================
package com.ABC.pioneer.sensor.service;
import android.content.Context;
import com.ABC.pioneer.sensor.client.controller.PioneerClient;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.net.ssl.SSLSocket;
public class Connection {
private Context context;
DataInputStream in = null;
DataOutputStream out = null;
public Connection() throws IOException {
}
public Connection(Context context) throws IOException {
this.context = context;
}
//连接至服务器
public void ConnectToServer() {
PioneerClient connect = new PioneerClient("95.179.230.181", 446, context);
final int handshakeTimeout=3*1000;
final int sessionTimeout=7*1000;
try {
SSLSocket socket = connect.run();
socket.setEnabledCipherSuites(socket.getSupportedCipherSuites());
// 开始握手
socket.setSoTimeout(handshakeTimeout);
socket.startHandshake();
socket.setSoTimeout(sessionTimeout);
// 建立连接后获取会话
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
}
}
public String Register(String phone, String secretKey) {
String str = "0";
try {
str += phone;
str += secretKey.toString();
byte[] out_bytes = new byte[str.length()];
out_bytes = str.getBytes();
//发送用户信息到服务器
out.write(out_bytes);
out.flush();
try {
//0表示注册成功,1表示手机号重复,2表示密钥重复
str = in.readUTF();
} catch (Exception e) {
}
} catch (IOException e) {
}
return str;
}
public String Token(String token) {
String str = "1";
try {
str += token;
byte[] out_bytes = new byte[str.length()];
out_bytes = str.getBytes();
//发送用户信息到服务器
out.write(out_bytes);
out.flush();
try {
str = in.readUTF();
} catch (Exception e) {
}
} catch (IOException e) {
}
return str;
}
public String Token_TransmitMachingKeys(String phone,String Maching_keys) throws IOException {
String str ="3";
str += phone;
str+=Maching_keys;
byte[] out_bytes=new byte[10000];
out_bytes=str.getBytes();
out.write(out_bytes);
out.flush();
str=in.readUTF();
return str;
}
public String TransmitMachingKeys(String phone,String Maching_keys) throws IOException {
String str ="4";
str += phone;
str+=Maching_keys;
byte[] out_bytes=new byte[10000];
out_bytes=str.getBytes();
out.write(out_bytes);
out.flush();
str=in.readUTF();
return str;
}
public String Download_Message() throws IOException {
String str = "5";
//根据输入的长度动态的分配
byte[] out_bytes = new byte[str.length()];
out_bytes = str.getBytes();
//发送用户信息到服务器
out.write(out_bytes);
out.flush();
str = in.readUTF();
return str;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/CustomTimer.java
================================================
package com.ABC.pioneer.sensor.service;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import java.util.Calendar;
import java.util.TimeZone;
public class CustomTimer extends Service {
public static final String UpdateDatabaseAction = "Pioneer.UpdateDatabase";
public static final String DownloadDataAction = "Pioneer.DownloadData";
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
setUpdateDatabase(this);
setDownloadData(this);
return super.onStartCommand(intent,flags,startId);
}
private void setUpdateDatabase(Context context)
{
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08"));
// 设置为下一天的零点
calendar.setTimeInMillis(System.currentTimeMillis()+24*60*60*1000);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Intent intent = new Intent();
intent.setAction(UpdateDatabaseAction);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0,intent,PendingIntent.FLAG_CANCEL_CURRENT);
manager.setExact(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pendingIntent);
}
private void setDownloadData(Context context)
{
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08"));
// 设置为下一天的8点
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Intent intent = new Intent();
intent.setAction(DownloadDataAction);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,1,intent,PendingIntent.FLAG_CANCEL_CURRENT);
manager.setExact(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pendingIntent);
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/ForegroundService.java
================================================
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0
//
package com.ABC.pioneer.sensor.service;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
/// Foreground service for enabling continuous BLE operation in background
public class ForegroundService extends Service {
public static final String ACTION_START = "ACTION_START_FOREGROUND_SERVICE";
public static final String ACTION_STOP = "ACTION_STOP_FOREGROUND_SERVICE";
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
String action = intent.getAction();
switch (action) {
case ACTION_START:
this.startForegroundService();
break;
case ACTION_STOP:
this.stopForegroundService();
break;
}
}
super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void startForegroundService() {
final NotificationService notificationService = NotificationService.shared(getApplication());
startForeground(notificationService.getForegroundServiceNotificationId(), notificationService.getForegroundServiceNotification());
}
private void stopForegroundService() {
stopForeground(true);
stopSelf();
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/MatchDelegate.java
================================================
package com.ABC.pioneer.sensor.service;
public interface MatchDelegate {
void matchFound();
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/NotificationService.java
================================================
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0
//
package com.ABC.pioneer.sensor.service;
import android.app.Application;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
/// 用于启用前台服务的通知服务(必须显示通知以显示应用程序正在后台运行)。
public class NotificationService {
private static NotificationService shared = null;
private static Application application = null;
private final Context context;
private int notificationId;
private Notification notification;
private NotificationService(final Application application) {
this.application = application;
this.context = application.getApplicationContext();
}
/// 获取通知服务的共享全局实例
public final static NotificationService shared(final Application application) {
if (shared == null) {
shared = new NotificationService(application);
}
return shared;
}
/// 启动前台服务开启后台扫描
public void startForegroundService(Notification notification, int notificationId) {
this.notification = notification;
this.notificationId = notificationId;
final Intent intent = new Intent(context, ForegroundService.class);
intent.setAction(ForegroundService.ACTION_START);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
/// 停止当前前台服务
public void stopForegroundService() {
final Intent intent = new Intent(context, ForegroundService.class);
intent.setAction(ForegroundService.ACTION_STOP);
context.startService(intent);
}
public Notification getForegroundServiceNotification() {
return this.notification;
}
public int getForegroundServiceNotificationId() {
return this.notificationId;
}
}
================================================
FILE: Android_app/pioneer/src/main/java/com/ABC/pioneer/sensor/service/PioneerDb.java
================================================
package com.ABC.pioneer.sensor.service;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import com.ABC.pioneer.sensor.datatype.Data;
import com.ABC.pioneer.sensor.datatype.PayloadData;
import com.ABC.pioneer.sensor.datatype.TimeInterval;
import com.ABC.pioneer.sensor.payload.Crypto.HMACSHA256;
import com.ABC.pioneer.sensor.payload.Crypto.SpecificUsePayloadSupplier;
import com.ABC.pioneer.sensor.payload.Crypto.ContactIdentifier;
import com.ABC.pioneer.sensor.payload.Crypto.ContactKey;
import com.ABC.pioneer.sensor.payload.Crypto.GenerateKey;
import com.ABC.pioneer.sensor.payload.Crypto.MatchingKey;
import java.nio.charset.StandardCharsets;
public class PioneerDb extends SQLiteOpenHelper{
private static final int table_num = 15; // 我们一共要存15天的表
private static final long periodMillis = TimeInterval.minutes(6).millis();
private static final long dayMillis = TimeInterval.minutes(24*60).millis();
// 第一个表存放的是当前天的数据,之后往前推
public PioneerDb(Context context, String name, CursorFactory factory,int version)
{
super(context,name,factory,version);
}
@Override
public void onCreate(SQLiteDatabase db)
{
for(int i=1;i<=table_num;i++)
{
String table_name = "payload" + i;
db.execSQL("create table "+ table_name +
" (id INTEGER PRIMARY KEY AUTOINCREMENT, contactidentifier VARCHAR(20), mac BLOB,rawdata BLOB)");
}
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion, int newVersion){
}
public int getTableNum()
{
return table_num;
}
// 清空当前数据库
public void deleteCurTable()
{
SQLiteDatabase database = getWritableDatabase();
database.beginTransaction();
String expTb = "payload" + 1;
database.execSQL("delete from " + expTb);
database.close();
}
//match contact identifier
private static boolean matchContactIdentifier(SQLiteDatabase database,int offset,MatchingKey matchingKey,ContactIdentifier contactIdentifier,int period)
{
// 取出所有contactIdentifier对应的记录
String tbName = "payload" + (offset+1);
String contactIdentifierStr = new String(contactIdentifier.value,StandardCharsets.UTF_8);
Cursor cursor = database.rawQuery("SELECT * FROM " + tbName + " WHERE contactidentifier = ?",new String[]{contactIdentifierStr});
while(cursor.moveToNext())
{
// 如果找到则进行mac验证
// mac验证
byte [] macBytes = cursor.getBlob(cursor.getColumnIndex("mac"));
byte [] rawdataBytes = cursor.getBlob(cursor.getColumnIndex("rawdata"));
try {
if (HMACSHA256.VerifyMAC(rawdataBytes, matchingKey.value, macBytes))
{
cursor.close();
return true;
}
cursor.close();
return true;
}
catch(Exception e)
{
cursor.close();
return false;
}
}
cursor.close();
return false;
}
// 插入数据
public void insertPayloadData(PayloadData payloadData) {
if (!(payloadData == null || payloadData.value.length == 0)) {
SQLiteDatabase database = getWritableDatabase();
// 解析payload数据
Data contactidentifer = SpecificUsePayloadSupplier.parseContactIdentifier(payloadData);
Data mac = SpecificUsePayloadSupplier.parseMac(payloadData);
Data rawdata = SpecificUsePayloadSupplier.parseRawData(payloadData);
// 插入到数据库的今天的表中
// 由于mac是乱码,所以直接存BLOB结构
ContentValues values = new ContentValues();
values.put("contactidentifier", new String(contactidentifer.value, StandardCharsets.UTF_8));
values.put("mac", mac.value);
values.put("rawdata", rawdata.value);
database.insert("payload1", null, values);
database.close();
}
}
// 根据matchingkey 以及 距离当日的offset天数 进行匹配
public boolean matchMatchingKey(MatchingKey matchingKey,int offset)
{
// 由matchingKey生成contactIdentifiers
final ContactKey[] contactKeys = GenerateKey.contactKeys(matchingKey);
final ContactIdentifier[] contactIdentifiers = new ContactIdentifier[contactKeys.length];
for (int i=contactKeys.length; i-->0;) {
contactIdentifiers[i] = GenerateKey.contactIdentifier(contactKeys[i]);
}
// 去对应的数据库表中查找有没有匹配的contactIdentifier
SQLiteDatabase db = getWritableDatabase();
for(int i=contactIdentifiers.length-1;i>=0;i--)
{
if(matchContactIdentifier(db,offset,matchingKey,contactIdentifiers[i],i))
return true;
}
db.close();
return false;
}
// 每天晚上12点更新
public void updateTable()
{
SQLiteDatabase database = getWritableDatabase();
database.beginTransaction();
try {
// 清空过期的表
String expTb = "payload" + table_num;
database.execSQL("delete from " + expTb);
// 先暂存这张表
database.execSQL("ALTER TABLE " + expTb + " RENAME TO " + "tmpTb");
// 前13张表后移
for(int i=table_num-1;i>=1;i--)
{
String old_Tb = "payload" + i;
String new_Tb = "payload" + (i+1);
database.execSQL("ALTER TABLE " + old_Tb + " RENAME TO " + new_Tb);
}
// 把暂存的表设为最新的表
database.execSQL("ALTER TABLE " + "tmpTb" + " RENAME TO " + "payload1");
database.setTransactionSuccessful();
}
finally
{
database.endTransaction();
database.close();
}
}
}
================================================
FILE: Android_app/pioneer/src/test/java/com/ABC/pioneer/sensor/ExampleUnitTest.java
================================================
package com.ABC.pioneer.sensor;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see Testing documentation
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: Android_app/settings.gradle
================================================
include ':pioneer'
include ':app'
rootProject.name = "Pioneer"
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/ContactLogData/ContactLog.xcdatamodeld/.xccurrentversion
================================================
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/ContactLogData/ContactLog.xcdatamodeld/ContactLog.xcdatamodel/contents
================================================
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/CipherParameters.swift
================================================
//
// CipherParameters.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2..
//
import Foundation
public protocol CipherParameters
{
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/Crypto.swift
================================================
//
// Crypto.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class Crypto {
//Hash generation
public static func Hash(message: Data) -> Data {
var Hash = [UInt8](repeating: 0,count: 32)
let pHash: PioneerHash = PioneerHash()
pHash.generateHash(from: [UInt8].init(message), to: &Hash)
return Data.init(Hash)
}
// HashMAC
public static func MAC(message: Data, key: Data) -> Data {
var MAC = [UInt8](repeating: 0, count: 32)
let pMAC = PioneerHMac(from: [UInt8].init(key))
pMAC.generateMac(from: [UInt8].init(message), to: &MAC)
return Data.init(MAC)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/Digest.swift
================================================
//
// Digest.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public protocol Digest
{
func getAlgorithmName() -> String
func getDigestSize() -> Int
func update(inbyte:UInt8)
func update(inbytes:Array,inOff:Int,inLen:Int)
func doFinal(outbytes:inout Array,outOff:Int) -> Int
func reset()
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/DigestRandomNumber.swift
================================================
//
// DigestRandomNumber.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class DigestRandomNumber : RandomGenerator
{
private static let CYCLE_COUNT:Int64 = 10
private var stateCounter:UInt64
private var seedCounter:UInt64
private var digest:Digest
private var state:[UInt8]
private var seed:[UInt8]
init(from digest:Digest)
{
self.digest = digest
self.seed = [UInt8](repeating:0,count:digest.getDigestSize())
self.seedCounter = 1
self.state = [UInt8](repeating:0,count:digest.getDigestSize())
self.stateCounter = 1
}
public func addSeedMaterial(from inSeed: [UInt8]) {
if(seed.count != 0)
{
digestUpdate(from:inSeed)
}
digestUpdate(from:self.seed)
digestDoFinal(to:&self.seed)
}
public func addSeedMaterial(from rSeed: UInt64) {
digestAddCounter(from:rSeed)
digestUpdate(from:self.seed)
digestDoFinal(to:&self.seed)
}
public func nextBytes(to bytes: inout [UInt8]) {
nextBytes(to: &bytes, start: 0, len: bytes.count)
}
public func nextBytes(to bytes: inout [UInt8], start: Int, len: Int) {
var stateOff:Int = 0
generateState()
let end:Int = start + len
for i in start..>= 8
}
}
private func digestUpdate(from inSeed:[UInt8])
{
digest.update(inbytes: inSeed, inOff: 0, inLen: inSeed.count)
}
private func digestDoFinal(to result:inout [UInt8])
{
let _ = digest.doFinal(outbytes: &result, outOff: 0)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/ExtendedDigest.swift
================================================
//
// ExtendedDigest.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public protocol ExtendedDigest : Digest
{
func getByteLength() -> Int
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/GeneralDigest.swift
================================================
//
// GeneralDigest.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class GeneralDigest : ExtendedDigest,Memoable
{
private static let BYTE_LENGTH:Int = 64
private var xBuf = Array(repeating:0,count:4)
private var xBufOff:Int = 0
private var byteCount:Int64 = 0
init() {}
init(from t:GeneralDigest)
{
copyIn(from:t)
}
init(from encodedState:Array)
{
xBuf[0..,inOff:Int,inLen:Int)
{
let len:Int = max(inLen,0)
var i:Int = 0
if(xBufOff != 0)
{
while(i Int
{
return Self.BYTE_LENGTH
}
func processWord(inbytes:Array,inOff:Int)
{
// to be overwritten
}
func processLength(bitlength:Int64)
{
// to be overwritten
}
func processBlock()
{
// to be overwritten
}
public func copy() -> Memoable {
return GeneralDigest(from:self)
}
public func reset(from other: Memoable) {
byteCount = (other as! GeneralDigest).byteCount
xBufOff = (other as! GeneralDigest).xBufOff
xBuf[0.. String {
return "GeneralDigest"
}
public func getDigestSize() -> Int {
return 0
}
public func doFinal(outbytes: inout Array, outOff: Int) -> Int {
return 0
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/HMAC.swift
================================================
//
// HMac.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class HMAC : MAC
{
private static let IPAD:UInt8 = 0x36
private static let OPAD:UInt8 = 0x5c
private var digest:Digest
private var digestSize:Int
private var blockLength:Int
private var ipadState:Memoable?
private var opadState:Memoable?
private var inputPad:[UInt8]
private var outputBuf:[UInt8]
private static var blockLengths:[String:Int] = {
var tmpdict = ["SM3":64]
return tmpdict
}()
private static func getByteLength(which digest:Digest) -> Int
{
return blockLengths[digest.getAlgorithmName()]!
}
convenience init(which digest:Digest)
{
self.init(which:digest,byteLength:Self.getByteLength(which:digest))
}
init(which digest:Digest,byteLength:Int)
{
self.digest = digest
self.digestSize = digest.getDigestSize()
self.blockLength = byteLength
self.inputPad = [UInt8](repeating:0,count:blockLength)
self.outputBuf = [UInt8](repeating:0,count:blockLength+digestSize)
}
public func getAlgorithmName() -> String {
return digest.getAlgorithmName() + "/HMAC"
}
public func getUnderlyingDigest() -> Digest
{
return digest
}
public func macinit(from params: CipherParameters) {
digest.reset()
let key:[UInt8] = (params as! KeyParameter).getKey()
var keyLength = key.count
if(keyLength > blockLength)
{
digest.update(inbytes: key, inOff: 0, inLen: keyLength)
let _ = digest.doFinal(outbytes: &inputPad, outOff: 0)
keyLength = digestSize
}
else
{
inputPad[0.. Int {
return digestSize
}
public func update(from inByte: UInt8) {
digest.update(inbyte: inByte)
}
public func update(from inBytes: [UInt8], inOff: Int, len: Int) {
digest.update(inbytes: inBytes, inOff: inOff, inLen: len)
}
public func doFinal(to out: inout [UInt8], outOff: Int) -> Int {
let _ = digest.doFinal(outbytes: &outputBuf, outOff: blockLength)
if(opadState != nil)
{
(digest as! Memoable).reset(from:opadState!)
digest.update(inbytes: outputBuf, inOff: blockLength, inLen: digest.getDigestSize())
}
else{
digest.update(inbytes: outputBuf, inOff: 0, inLen: outputBuf.count)
}
let len:Int = digest.doFinal(outbytes: &out, outOff: outOff)
for i in blockLength.. [UInt8]
{
return key
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/MAC.swift
================================================
//
// Mac.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
protocol MAC
{
func macinit(from params:CipherParameters)
func getAlgorithmName() -> String
func getMacSize() -> Int
func update(from inByte:UInt8)
func update(from inBytes:[UInt8],inOff:Int,len:Int)
func doFinal(to out:inout [UInt8],outOff:Int) -> Int
func reset()
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/Memoable.swift
================================================
//
// Memoable.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public protocol Memoable
{
func copy() -> Memoable
func reset(from other:Memoable)
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/PioneerHMac.swift
================================================
//
// PioneerHMac.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class PioneerHMac
{
var digest: Digest
var hmac: MAC
init(from key: [UInt8])
{
digest = SM3Digest()
hmac = HMAC(which:digest)
hmac.macinit(from: KeyParameter(from:key))
}
public func generateMac(from message: [UInt8],to mac: inout [UInt8])
{
generateMac(from: message, inOff: 0, len: message.count, to: &mac, outOff: 0)
}
public func generateMac(from message: [UInt8], inOff: Int, len: Int, to mac: inout [UInt8], outOff: Int)
{
hmac.update(from: message, inOff: inOff, len: len)
_ = hmac.doFinal(to: &mac, outOff: outOff)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/PioneerHash.swift
================================================
//
// PioneerHash.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class PioneerHash
{
var digest:Digest
init()
{
digest = SM3Digest()
}
public func generateHash(from message:[UInt8],to hash:inout [UInt8])
{
generateHash(from: message, inOff: 0, len: message.count, hash: &hash, outOff: 0)
}
public func generateHash(from message:[UInt8],inOff:Int,len:Int,hash:inout [UInt8],outOff:Int)
{
digest.update(inbytes: message, inOff: inOff, inLen: len)
_ = digest.doFinal(outbytes: &hash, outOff: outOff)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/RandomGenerator.swift
================================================
//
// RandomGenerator.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public protocol RandomGenerator
{
func addSeedMaterial(from seed: [UInt8])
func addSeedMaterial(from seed: UInt64)
func nextBytes(to bytes: inout [UInt8])
func nextBytes(to bytes: inout [UInt8], start: Int, len: Int)
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/SM3Digest.swift
================================================
//
// SM3Digest.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class SM3Digest : GeneralDigest
{
private var V = Array(repeating:0,count:DIGEST_LENGTH/4)
private var inwords = Array(repeating:0,count:BLOCK_SIZE)
private var xOff:Int = 0
private var W = Array(repeating:0,count:68)
private static let DIGEST_LENGTH:Int = 32
private static let BLOCK_SIZE:Int = 64/4
private func P0(x:UInt32) -> UInt32
{
let r9:UInt32 = ((x<<9) | (x>>(32-9)))
let r17:UInt32 = ((x<<17) | (x>>(32-17)))
return x^r9^r17
}
private func P1(x:UInt32) -> UInt32
{
let r15:UInt32 = ((x<<15) | x>>(32-15))
let r23:UInt32 = ((x<<23) | x>>(32-23))
return x^r15^r23
}
private func FF0(x:UInt32,y:UInt32,z:UInt32) -> UInt32
{
return x^y^z
}
private func FF1(x:UInt32,y:UInt32,z:UInt32) -> UInt32
{
return ((x&y) | (x&z) | (y&z))
}
private func GG0(x:UInt32,y:UInt32,z:UInt32) -> UInt32
{
return x^y^z
}
private func GG1(x:UInt32,y:UInt32,z:UInt32) -> UInt32
{
return ((x&y) | ((~x)&z))
}
private static var T:Array = {
var init_t = Array(repeating:0,count:64)
for i in 0..<16
{
let t:UInt32 = 0x79cc4519
init_t[i] = (t<>(32-i))
}
for i in 16..<64
{
let n:Int = i % 32
let t:UInt32 = 0x7a879d8a
init_t[i] = (t<>(32-n))
}
return init_t
}()
override init()
{
super.init()
reset()
}
init(from t:SM3Digest)
{
super.init(from:t)
copyIn(from:t)
}
fileprivate func copyIn(from t:SM3Digest)
{
V[0.. String {
return "SM3"
}
public override func getDigestSize() -> Int {
return Self.DIGEST_LENGTH
}
public override func copy() -> Memoable {
return SM3Digest(from:self)
}
public override func reset(from other: Memoable) {
let d = SM3Digest(from:other as! SM3Digest)
super.copyIn(from:d)
copyIn(from:d)
}
public override func doFinal(outbytes: inout Array, outOff: Int) -> Int {
finish()
Util.intToBigEndian(srcIntArray: V, desByteArray: &outbytes, offset: outOff)
reset()
return Self.DIGEST_LENGTH
}
override func processWord(inbytes: Array, inOff: Int) {
let n:UInt32 = ((UInt32(inbytes[inOff] & 0xff)<<24) |
(UInt32(inbytes[inOff+1] & 0xff)<<16) |
(UInt32(inbytes[inOff+2] & 0xff)<<8) |
(UInt32(inbytes[inOff+3] & 0xff)))
inwords[xOff] = n
xOff += 1
if(xOff>=16)
{
processBlock()
}
}
override func processLength(bitlength: Int64) {
if(xOff > (Self.BLOCK_SIZE - 2))
{
inwords[xOff] = 0
xOff += 1
processBlock()
}
while(xOff < (Self.BLOCK_SIZE-2))
{
inwords[xOff] = 0
xOff += 1
}
inwords[xOff] = UInt32(bitlength)>>32
xOff += 1
inwords[xOff] = UInt32(bitlength)
xOff += 1
}
public override func reset()
{
super.reset()
V[0] = 0x7380166f
V[1] = 0x4914b2b9
V[2] = 0x172442d7
V[3] = 0xda8a0600
V[4] = 0xa96f30bc
V[5] = 0x163138aa
V[6] = 0xe38dee4d
V[7] = 0xb0fb0e4e
xOff=0
}
override func processBlock()
{
for j in 0..<16
{
W[j] = inwords[j]
}
for j in 16..<68
{
let wj3:UInt32 = W[j-3]
let r15:UInt32 = ((wj3<<15) | (wj3>>(32-15)))
let wj13:UInt32 = W[j-13]
let r7:UInt32 = ((wj13<<7) | wj13>>(32-7))
W[j] = P1(x:W[j-16]^W[j-9]^r15)^r7^W[j-6]
}
var A:UInt32 = V[0]
var B:UInt32 = V[1]
var C:UInt32 = V[2]
var D:UInt32 = V[3]
var E:UInt32 = V[4]
var F:UInt32 = V[5]
var G:UInt32 = V[6]
var H:UInt32 = V[7]
for j in 0..<16
{
let a12:UInt32 = ((A<<12) | A>>(32-12))
let s1_:UInt32 = a12 &+ E &+ Self.T[j]
let SS1:UInt32 = ((s1_<<7) | (s1_>>(32-7)))
let SS2:UInt32 = SS1 ^ a12
let Wj:UInt32 = W[j]
let W1j:UInt32 = Wj ^ W[j+4]
let TT1:UInt32 = FF0(x:A,y:B,z:C) &+ D &+ SS2 &+ W1j
let TT2 = GG0(x:E,y:F,z:G) &+ H &+ SS1 &+ Wj
D = C
C = ((B<<9) | (B>>(32-9)))
B = A
A = TT1
H = G
G = ((F<<19) | F>>(32-19))
F = E
E = P0(x:TT2)
}
for j in 16..<64
{
let a12:UInt32 = ((A<<12) | A>>(32-12))
let s1_ = a12 &+ E &+ Self.T[j]
let SS1:UInt32 = ((s1_<<7) | (s1_>>(32-7)))
let SS2:UInt32 = SS1^a12
let Wj:UInt32 = W[j]
let W1j:UInt32 = Wj^W[j+4]
let TT1 = FF1(x:A,y:B,z:C) &+ D &+ SS2 &+ W1j
let TT2 = GG1(x:E,y:F,z:G) &+ H &+ SS1 &+ Wj
D = C
C = ((B<<9) | B>>(32-9))
B = A
A = TT1
H = G
G = ((F<<19) | F>>(32-19))
F = E
E = P0(x:TT2)
}
V[0] ^= A
V[1] ^= B
V[2] ^= C
V[3] ^= D
V[4] ^= E
V[5] ^= F
V[6] ^= G
V[7] ^= H
xOff = 0
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Crypto/Util.swift
================================================
//
// Util.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class Util
{
public static func bigEndianToInt(srcByteArray bs:Array,offset off:Int) -> UInt32
{
var n:UInt32 = UInt32(bs[off] << 24)
n |= UInt32((bs[off+1] & 0xff)<<16)
n |= UInt32((bs[off+2] & 0xff)<<8)
n |= UInt32(bs[off+3] & 0xff)
return n
}
public static func intToBigEndian(srcInt n:UInt32,desByteArray bs:inout Array,offset off:Int)
{
bs[off] = UInt8(truncatingIfNeeded:n>>24)
bs[off+1] = UInt8(truncatingIfNeeded:n>>16)
bs[off+2] = UInt8(truncatingIfNeeded:n>>8)
bs[off+3] = UInt8(truncatingIfNeeded:n)
}
public static func intToBigEndian(srcIntArray ns:Array,desByteArray bs:inout Array,offset off:Int)
{
var offset = off
for i in 0..,offset off:Int) -> UInt64
{
let hi:UInt32 = bigEndianToInt(srcByteArray:bs,offset:off)
let lo:UInt32 = bigEndianToInt(srcByteArray:bs,offset:off+4)
return UInt64((hi & 0xffffffff)<<32) | UInt64(lo & 0xffffffff)
}
public static func longToBigEndian(srcLong n:UInt64,desByteArray bs:inout Array,offset off:Int)
{
intToBigEndian(srcInt:UInt32(n >> 32),desByteArray:&bs,offset:off)
intToBigEndian(srcInt:UInt32(n & 0xffffffff),desByteArray:&bs,offset:off+4)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Manager/Connection.swift
================================================
//
// Connection.swift
// Pioneer
//
// Created by Beh on 2025/3/2.
//
import Network
import Foundation
public class Connection{
private let queue: DispatchQueue
private let host: NWEndpoint.Host
private let port: NWEndpoint.Port
public var connection: NWConnection?
public var decription: String { get {
return "Not set"
}
}
public init(queue: DispatchQueue){
self.queue = queue
self.host = NWEndpoint.Host.init(GlobalConfiguration.serverHost)
self.port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(GlobalConfiguration.serverPort))
self.connection = NWConnection.init(host: host, port: port, using: createTLSParameters(queue: queue))
}
public init(host:String,port:IntegerLiteralType,queue:DispatchQueue){
self.queue = queue
self.host = NWEndpoint.Host.init(host)
self.port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(port))
self.connection = NWConnection.init(host: self.host, port: self.port, using: createTLSParameters(queue: self.queue))
}
public func start(){
connection?.stateUpdateHandler = { newState in
switch newState{
case .ready:
print("state ready")
case .cancelled:
print("state cancel")
case .waiting(let error):
print("state waiting \(error)")
case .failed(let error):
print("state failed \(error)")
default:
break
}
}
connection?.start(queue: queue)
}
public func state() -> NWConnection.State?{
return connection?.state
}
public func restart(){
connection?.restart()
}
public func cancel(){
connection?.cancel()
}
public func send(dataMessage:String) -> Bool{
var isSuccess: Bool = false
let semaphore = DispatchSemaphore(value: 0)
let processedData: Data = dataMessage.data(using: .utf8)!
connection?.send(content: processedData, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({error in
guard error == nil else{
print("Send Error Message:\(String(describing: error?.localizedDescription))")
semaphore.signal()
return
}
print("Send Success!")
isSuccess = true
semaphore.signal()
}))
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
return isSuccess
}
public func recieve() -> String?{
let semaphore = DispatchSemaphore(value: 0)
var recievedValue: String? = nil
connection?.receive(minimumIncompleteLength: 0, maximumLength: 65535, completion:{ (recieveData, contentContext, isBool, error) in
guard error == nil else{
print("Recieve Error Message:\(String(describing: error?.localizedDescription))")
semaphore.signal()
return
}
print("Recieve Success!")
print("ContentContext:" + contentContext!.identifier)
guard let recieveMessage = recieveData else{
semaphore.signal()
return
}
if let recievedString = String.init(data: recieveMessage.suffix(from: 2), encoding: .utf8) {
print("Recieve message:" + recievedString)
recievedValue = recievedString
}
semaphore.signal()
})
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
return recievedValue
}
private func createTLSParameters(queue: DispatchQueue) -> NWParameters{
let options = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(options.securityProtocolOptions,{(sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
var error: CFError?
if SecTrustEvaluateWithError(trust, &error){
sec_protocol_verify_complete(true)
}else{
let certificate = SecTrustGetCertificateAtIndex(trust, 0)!
let remoteCeritificateData = CFBridgingRetain(SecCertificateCopyData(certificate))!
let cerPath = Bundle.main.path(forResource: GlobalConfiguration.serverCertificate, ofType: "cer")!
let cerUrl = URL(fileURLWithPath: cerPath)
let localCertificateData: Data = try! Data(contentsOf: cerUrl)
if (remoteCeritificateData.isEqual(localCertificateData) == true){
sec_protocol_verify_complete(true)
}else{
sec_protocol_verify_complete(false)
}
}}, queue)
sec_protocol_options_set_challenge_block(options.securityProtocolOptions, {(sec_protocol_metadata_t, sec_protocol_challenge_complete_t) in
var securityError: OSStatus = errSecSuccess
let path: String = Bundle.main.path(forResource: GlobalConfiguration.clientCertificate, ofType: "p12")!
let PKCS12Data = NSData(contentsOfFile: path)!
let key: NSString = kSecImportExportPassphrase as NSString
let key_options: NSDictionary = [key:GlobalConfiguration.clientCertificatePass]
var items: CFArray?
securityError = SecPKCS12Import(PKCS12Data, key_options, &items)
if securityError == errSecSuccess{
let certItems: CFArray = items!
let cerItemsArray: Array = certItems as Array
let dict: AnyObject? = cerItemsArray.first
if let certEntry: Dictionary = dict as? Dictionary{
let identityPointer: AnyObject? = certEntry["identity"]
let secIdentityRef: SecIdentity = (identityPointer as! SecIdentity?)!
let secIdentity: sec_identity_t? = sec_identity_create(secIdentityRef)
sec_protocol_challenge_complete_t(secIdentity)
}
}
}, queue)
return NWParameters(tls: options)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Manager/GlobalConfiguration.swift
================================================
//
// GlobalConfiguration.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public struct GlobalConfiguration{
//默认服务器地址为95.179.230.181//
public static let serverHost: String = "95.179.230.181"
public static let serverPort: Int = 446
public static let serverCertificate: String = "server"
public static let clientCertificate: String = "client"
public static let clientCertificatePass: String = "123456"
public static let TestForAutoDownload: Bool = false
public static let TimeIntervalForAutoDownload: TimeInterval = 12 * TimeInterval.hour + 0 * TimeInterval.minute
}
public class InsideEncounter {
public let startTime: Date
public let mac: String
public let period: Int16
public let contactIdentifier: String
public init(_ startTime: Date, _ mac: String, _ period: Int16, _ contactIdentifier: String){
self.startTime = startTime
self.mac = mac
self.period = period
self.contactIdentifier = contactIdentifier
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Manager/KeyManager.swift
================================================
//
// KeyManager.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
import CommonCrypto
import Accelerate
public typealias SecretKey = Data
public typealias MatchingKey = Data
public typealias ContactIdentifier = Data
/// 密钥派生函数
public class KeyManager {
private static let secretKeyLength = 2048
private static let days = 2000
private static let periods = 240
private static let epoch = KeyManager.getEpoch()
static func date(_ fromString: String) -> Date? {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXX"
return formatter.date(from: fromString)
}
static func getEpoch() -> TimeInterval {
return date("2020-09-01T00:00:00+0000")!.timeIntervalSince1970
}
public static func day(_ onDate: Date = Date()) -> Int {
let (day,_) = Int(onDate.timeIntervalSince1970 - epoch).dividedReportingOverflow(by: 86400)
return day
}
public static func period(_ atTime: Date = Date()) -> Int {
let (second,_) = Int(atTime.timeIntervalSince1970 - epoch).remainderReportingOverflow(dividingBy: 86400)
let (period,_) = second.dividedReportingOverflow(by: 86400 / periods)
return period
}
public static func endTime(atTime: Date) -> UInt32{
let (second,_) = Int(atTime.timeIntervalSince1970).remainderReportingOverflow(dividingBy: 86400)
let (period,_) = second.dividedReportingOverflow(by: 86400 / periods)
return UInt32(Int(atTime.timeIntervalSince1970) - second + (period + 1) * 360)
}
static func secretKey() -> SecretKey? {
var bytes = [UInt8](repeating: 0, count: secretKeyLength)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
guard status == errSecSuccess else {
return nil
}
return SecretKey(bytes)
}
static func matchingKeys(_ secretKey: SecretKey) -> [MatchingKey] {
let n = KeyManager.days
//面向安全的匹配密钥种子由一个带截断的反向哈希链生成,以确保将来的密钥不能从历史密钥派生。加密散列函数为前向安全性提供了一个单向函数。截断函数通过删除中间密钥材料提供额外的保证,因此受损的哈希函数仍将保持前向安全性。
var matchingKeySeed: [MatchingKeySeed] = Array(repeating: MatchingKeySeed(), count: n + 1)
//最后一个匹配的密钥种子在2000天(从epoch算起超过5年)是密钥的散列。在2000天耗尽所有匹配的密钥种子之前,需要建立一个新的密钥。
matchingKeySeed[n] = MatchingKeySeed(Crypto.Hash(message: secretKey))
for i in (0...n - 1).reversed() {
matchingKeySeed[i] = MatchingKeySeed(Crypto.Hash(message: KeyF.t(matchingKeySeed[i + 1])))
}
//第i天的匹配密钥是第i天与第i-1天的匹配密钥种子的异或的哈希。有必要将匹配密钥与其种子分离,因为在分散的联系人跟踪解决方案中,匹配密钥由服务器分发给所有手机进行设备上匹配。如果一个种子用于派生其他日期的种子,则发布哈希可防止攻击者建立其他种子。
var matchingKey: [MatchingKey] = Array(repeating: MatchingKey(), count: n + 1)
for i in 1...n {
matchingKey[i] = MatchingKey(Crypto.Hash(message: KeyF.xor(matchingKeySeed[i], matchingKeySeed[i - 1])))
}
//第0天的匹配密钥派生自第0天和第- 1天的匹配密钥种子。在上面的代码中实现为特例
let matchingKeySeedMinusOne = MatchingKeySeed(Crypto.Hash(message: KeyF.t(matchingKeySeed[0])))
matchingKey[0] = MatchingKey(Crypto.Hash(message: KeyF.xor(matchingKeySeed[0], matchingKeySeedMinusOne)))
return matchingKey
}
public static func contactKeys(_ matchingKey: MatchingKey) -> [ContactKey] {
let n = KeyManager.periods
/**
面向安全接触密钥种子是由一个带有截断的反向哈希链生成的,以确保将来的密钥不能从历史密钥中派生。这与生成匹配密钥种子的过程相同。种子永远不会通过手机传送。它们在密码学上具有挑战性,难以从广播接触人密钥中泄露,而在给定匹配密钥或私密密钥的情况下很容易生成。
*/
var contactKeySeed: [ContactKeySeed] = Array(repeating: ContactKeySeed(), count: n + 1)
contactKeySeed[n] = Crypto.Hash(message: matchingKey)
for j in (0...n - 1).reversed() {
contactKeySeed[j] = ContactKeySeed(Crypto.Hash(message: KeyF.t(contactKeySeed[j + 1])))
}
var contactKey: [ContactKey] = Array(repeating: ContactKey(), count: n + 1)
for j in 1...n {
contactKey[j] = ContactKey(Crypto.Hash(message: KeyF.xor(contactKeySeed[j], contactKeySeed[j - 1])))
}
let contactKeySeedMinusOne = ContactKeySeed(Crypto.Hash(message: KeyF.t(contactKeySeed[0])))
contactKey[0] = ContactKey(Crypto.Hash(message: KeyF.xor(contactKeySeed[0], contactKeySeedMinusOne)))
return contactKey
}
public static func contactIdentifier(_ contactKey: ContactKey) -> ContactIdentifier {
return ContactIdentifier(KeyF.t(contactKey, 16))
}
static func fortnightMatching(secretKey: SecretKey,time: Date) -> String{
var description: String = ""
let matching: [MatchingKey] = KeyManager.matchingKeys(secretKey)
let day: Int = KeyManager.day(time)
for index in day-13...day{
description.append(matching[index].base64EncodedString())
}
return description
}
}
// class KeyF 用于计算
private class KeyF {
fileprivate static func h(_ data: Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes({ _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) })
return Data(hash)
}
/// 截断函数:删除数据的后半部分
fileprivate static func t(_ data: Data) -> Data {
return KeyF.t(data, data.count / 2)
}
/// 截断函数:保留数据的前n个字节
fileprivate static func t(_ data: Data, _ n: Int) -> Data {
return data.subdata(in: 0.. Data {
let leftByteArray: [UInt8] = Array(left)
let rightByteArray: [UInt8] = Array(right)
var resultByteArray: [UInt8] = [UInt8]()
for i in 0.. [InsideEncounter]
}
public protocol TestManagerDelegate{
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Manager/Register.swift
================================================
//
// Register.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class Register: NSObject{
private let queue: DispatchQueue = DispatchQueue.init(label: "Register!")
private var phoneNumber: String
private var secretKey: SecretKey
public var delegate: RegisterDelegate? = nil
public init(_ phone: String,_ delegate: RegisterDelegate){
self.phoneNumber = phone
self.secretKey = KeyManager.secretKey()!
self.delegate = delegate
}
public func didClickConfirm() -> (Bool,UInt8){
var resultAsk: String?
let registerConnection = Connection.init(queue: queue)
if self.phoneNumber == "18239709939"{
register()
return (true,4)
}
while true{
let body: String = "0\(self.phoneNumber)\(self.secretKey.base64EncodedString())"
registerConnection.start()
guard registerConnection.send(dataMessage: body) else {
print("与服务器交互失败!")
registerConnection.cancel()
return (false,0)
}
resultAsk = registerConnection.recieve()
if resultAsk != "2"{
break
}
self.secretKey = KeyManager.secretKey()!
}
registerConnection.cancel()
guard let resultMessage = resultAsk else {
return (false,0)
}
switch resultMessage {
case "0":
register()
return (true,1)
case "1":
return (false,2)
default:
return (false,3)
}
}
public func register(){
let userDefault = UserDefaults.standard
userDefault.setValue(phoneNumber, forKey: "PhoneNumber")
userDefault.setValue(secretKey.base64EncodedString(), forKey: "SecretKey")
userDefault.setValue(true, forKey: "State")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
self.delegate?.didRegister()
}
}
public static func isRegister() -> Bool{
guard UserDefaults.standard.string(forKey: "PhoneNumber") == nil || UserDefaults.standard.string(forKey: "SecretKey") == nil || UserDefaults.standard.string(forKey: "State") == nil else{
return true
}
return false
}
public static func dangerState(){
UserDefaults.standard.setValue(false, forKey: "State")
}
public static func secrueState(){
UserDefaults.standard.setValue(true, forKey: "State")
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Manager/UploadManager.swift
================================================
//
// UploadManager.swift
// Pioneer for IOS
//
// Created by Beh on 2025/3/2.
//
import Foundation
public class UploadManager{
//自动化上传PhoneNumber和14天MatchingKeys
public static func uploadDataAuto(queue:DispatchQueue, time: Date = Date()) -> Bool{
var body:String = "4"
body.append(UserDefaults.standard.string(forKey: "PhoneNumber")!)
let secretKey: SecretKey = SecretKey.init(base64Encoded: UserDefaults.standard.string(forKey: "SecretKey")!)!
body.append(KeyManager.fortnightMatching(secretKey: secretKey, time: time))
let upLoadConnection = Connection.init(queue: queue)
upLoadConnection.start()
guard upLoadConnection.send(dataMessage: body) else{
upLoadConnection.cancel()
return false
}
upLoadConnection.cancel()
return true
}
//Token模式下上传PhoneNumber和14天MatchingKeys
public static func uploadDataToken(queue:DispatchQueue,time: Date = Date(),upLoadConnection: Connection) -> Bool{
var body:String = "3"
body.append(UserDefaults.standard.string(forKey: "PhoneNumber")!)
let secretKey: SecretKey = SecretKey.init(base64Encoded: UserDefaults.standard.string(forKey: "SecretKey")!)!
body.append(KeyManager.fortnightMatching(secretKey: secretKey, time: time))
guard upLoadConnection.send(dataMessage: body) else{
upLoadConnection.cancel()
return false
}
guard let response = upLoadConnection.recieve(),response == "0" else{
upLoadConnection.cancel()
return false
}
upLoadConnection.cancel()
return true
}
//自动从服务器下载风险者列表
public static func downloadRiskIdentifier(queue:DispatchQueue,time: Date = Date()) -> [MatchingKey]{
let body:String = "5"
var matchingKeysList: [MatchingKey] = []
let downloadConnection = Connection.init(queue: queue)
downloadConnection.start()
guard downloadConnection.send(dataMessage: body) else{
downloadConnection.cancel()
return matchingKeysList
}
guard var response = downloadConnection.recieve() else{
downloadConnection.cancel()
return matchingKeysList
}
//处理收到的列表
downloadConnection.cancel()
var header = response.prefix(3)
var matchingKeysStringList: [String] = []
while !header.isEmpty && response.count > 46{
if header == "ABC"{
response.removeSubrange(response.startIndex..
//! Project version number for Pioneer.
FOUNDATION_EXPORT double PioneerVersionNumber;
//! Project version string for Pioneer.
FOUNDATION_EXPORT const unsigned char PioneerVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/BLE/BLEDatabase.swift
================================================
//
// BLEDatabase.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreBluetooth
///BLEDatabase数据库用于存储所有探测设备的注册表
protocol BLEDatabase {
///添加用于处理数据库事件的委托
func add(delegate: BLEDatabaseDelegate)
///获取或创建设备
func device(_ identifier: TargetIdentifier) -> BLEDevice
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice
func device(_ payload: PayloadData) -> BLEDevice
///判断某设备是否存在,以有效负载数据为基准
func hasDevice(_ payload: PayloadData) -> Bool
///获取数据库中的全部设备
func devices() -> [BLEDevice]
/// 从数据库中删除设备
func delete(_ identifier: TargetIdentifier)
}
/// 用于接收注册表创建/更新/删除事件的委托
protocol BLEDatabaseDelegate {
func bleDatabase(didCreate device: BLEDevice)
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute)
func bleDatabase(didDelete device: BLEDevice)
}
extension BLEDatabaseDelegate {
func bleDatabase(didCreate device: BLEDevice) {}
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute) {}
func bleDatabase(didDelete device: BLEDevice) {}
}
class SpecificBLEDatabase : NSObject, BLEDatabase, BLEDeviceDelegate {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "BLE.SpecificBLEDatabase")
private var delegates: [BLEDatabaseDelegate] = []
private var database: [TargetIdentifier : BLEDevice] = [:]
private var queue = DispatchQueue(label: "Sensor.SpecificBLEDatabase")
func add(delegate: BLEDatabaseDelegate) {
delegates.append(delegate)
}
func devices() -> [BLEDevice] {
return database.values.map { $0 }
}
func device(_ identifier: TargetIdentifier) -> BLEDevice {
if database[identifier] == nil {
let device = BLEDevice(identifier, delegate: self)
database[identifier] = device
queue.async {
self.logger.debug("create (device=\(identifier))")
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
}
}
let device = database[identifier]!
return device
}
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice {
let identifier = TargetIdentifier(peripheral: peripheral)
if database[identifier] == nil {
let device = BLEDevice(identifier, delegate: self)
database[identifier] = device
queue.async {
self.logger.debug("create (device=\(identifier))")
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
}
}
let device = database[identifier]!
if device.peripheral != peripheral {
device.peripheral = peripheral
peripheral.delegate = delegate
}
return device
}
func device(_ payload: PayloadData) -> BLEDevice {
if let device = database.values.filter({ $0.payloadData == payload }).first {
return device
}
// 创建临时UUID以填充,taskRemoveDuplicatePeripherals函数将在与外围设备的直接连接已建立时删除此项
let identifier = TargetIdentifier(UUID().uuidString)
let placeholder = device(identifier)
placeholder.payloadData = payload
return placeholder
}
func hasDevice(_ payload: PayloadData) -> Bool {
if database.values.filter({ $0.payloadData == payload }).first != nil {
return true
}
return false
}
func delete(_ identifier: TargetIdentifier) {
guard let device = database[identifier] else {
return
}
database[identifier] = nil
queue.async {
self.logger.debug("delete (device=\(identifier))")
self.delegates.forEach { $0.bleDatabase(didDelete: device) }
}
}
// MARK:- BLEDeviceDelegate
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute) {
queue.async {
self.logger.debug("update (device=\(device.identifier),attribute=\(attribute.rawValue))")
self.delegates.forEach { $0.bleDatabase(didUpdate: device, attribute: attribute) }
}
}
}
// MARK:- BLEDatabase data
public class BLEDevice : Device {
/// 上次从此设备唤醒的时间(仅限iOS)
var lastNotifiedAt: Date = Date.distantPast
/// 伪设备地址,用于跟踪不断更改设备标识符的设备,如三星A10、A20和Note8
var pseudoDeviceAddress: BLEPseudoDeviceAddress? {
didSet {
lastUpdatedAt = Date()
}}
/// 用于侦听属性更新事件的委托。
let delegate: BLEDeviceDelegate
/// 用于与此设备交互的CoreBluetooth外围对象。
var peripheral: CBPeripheral? {
didSet {
lastUpdatedAt = Date()
delegate.device(self, didUpdate: .peripheral)
}}
/// 在设备之间发送信号的服务特性,例如保持唤醒
var signalCharacteristic: CBCharacteristic? {
didSet {
if signalCharacteristic != nil {
lastUpdatedAt = Date()
}
delegate.device(self, didUpdate: .signalCharacteristic)
}}
/// 读取有效负载数据的服务特性
var payloadCharacteristic: CBCharacteristic? {
didSet {
if payloadCharacteristic != nil {
lastUpdatedAt = Date()
}
delegate.device(self, didUpdate: .payloadCharacteristic)
}}
/// 设备操作系统,这是为每个平台选择不同交互程序所必需的。
var operatingSystem: BLEDeviceOperatingSystem = .unknown {
didSet {
lastUpdatedAt = Date()
delegate.device(self, didUpdate: .operatingSystem)
}}
/// 设备仅接收,这对于过滤有效负载共享数据是必需的
var receiveOnly: Bool = false {
didSet {
lastUpdatedAt = Date()
}}
/// 通过payloadCharacteristic read从设备获取的有效负载数据
var payloadData: PayloadData? {
didSet {
payloadDataLastUpdatedAt = Date()
lastUpdatedAt = payloadDataLastUpdatedAt
delegate.device(self, didUpdate: .payloadData)
}}
/// 有效负载数据上次更新时间戳,用于确定需要与对等方共享的内容。
var payloadDataLastUpdatedAt: Date = Date.distantPast
/// 已与此对等方共享的负载数据
var payloadSharingData: [PayloadData] = []
/// 由readRSSI或didDiscover进行的最新RSSI测量。
public var rssi: BLE_RSSI? {
didSet {
lastUpdatedAt = Date()
rssiLastUpdatedAt = lastUpdatedAt
delegate.device(self, didUpdate: .rssi)
}}
/// RSSI最后一次更新的时间戳,用于跟踪上一次广播的时间,而不依赖于didDiscover
var rssiLastUpdatedAt: Date = Date.distantPast
/// 在可用的情况下传输功率数据(仅由Android设备提供)
public var txPower: BLE_TxPower? {
didSet {
lastUpdatedAt = Date()
delegate.device(self, didUpdate: .txPower)
}}
/// 发射功率作为校准数据
var calibration: Calibration? { get {
guard let txPower = txPower else {
return nil
}
return Calibration(unit: .BLETransmitPower, value: Double(txPower))
}}
/// 在某时间戳发现的跟踪对象,由taskConnect用于在设备的并发连接容量不足时对连接进行优先级排序
var lastDiscoveredAt: Date = Date.distantPast
var lastConnectionInitiationAttempt: Date?
var lastConnectRequestedAt: Date = Date.distantPast
/// 上次连接请求时间戳
var lastConnectedAt: Date? {
didSet {
lastDisconnectedAt = nil
lastConnectionInitiationAttempt = nil
}}
var lastReadPayloadRequestedAt: Date = Date.distantPast
var lastDisconnectedAt: Date? {
didSet {
lastConnectionInitiationAttempt = nil
}
}
var lastAdvertAt: Date { get {
max(createdAt, lastDiscoveredAt, payloadDataLastUpdatedAt, rssiLastUpdatedAt)
}}
var timeIntervalSinceCreated: TimeInterval { get {
Date().timeIntervalSince(createdAt)
}}
/// 自上次属性值更新以来的时间间隔,用于标识可能已过期并应从数据库中删除的设备。
var timeIntervalSinceLastUpdate: TimeInterval { get {
Date().timeIntervalSince(lastUpdatedAt)
}}
/// 自上次有效负载数据更新以来的时间间隔,用于标识需要有效负载更新的设备。
var timeIntervalSinceLastPayloadDataUpdate: TimeInterval { get {
Date().timeIntervalSince(payloadDataLastUpdatedAt)
}}
/// 自上次检测到广播以来的时间间隔,用于检测并发连接配额并确定断开连接的优先级
var timeIntervalSinceLastAdvert: TimeInterval { get {
Date().timeIntervalSince(lastAdvertAt)
}}
/// 上次连接请求之间的时间间隔,用于优先断开连接
var timeIntervalSinceLastConnectRequestedAt: TimeInterval { get {
Date().timeIntervalSince(lastConnectRequestedAt)
}}
/// 最后一次连接和最后一个广播之间的时间间隔,用于估计连续跟踪的最后一个周期,以优先断开连接
var timeIntervalSinceLastDisconnectedAt: TimeInterval { get {
guard let lastDisconnectedAt = lastDisconnectedAt else {
return Date().timeIntervalSince(createdAt)
}
return Date().timeIntervalSince(lastDisconnectedAt)
}}
var timeIntervalBetweenLastConnectedAndLastAdvert: TimeInterval { get {
guard let lastConnectedAt = lastConnectedAt, lastAdvertAt > lastConnectedAt else {
return TimeInterval(0)
}
return lastAdvertAt.timeIntervalSince(lastConnectedAt)
}}
public override var description: String { get {
return "BLEDevice[id=\(identifier),os=\(operatingSystem.rawValue),payload=\(payloadData?.shortName ?? "nil"),address=\(pseudoDeviceAddress?.data.base64EncodedString() ?? "nil")]"
}}
init(_ identifier: TargetIdentifier, delegate: BLEDeviceDelegate) {
self.delegate = delegate
super.init(identifier);
}
}
protocol BLEDeviceDelegate {
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute)
}
enum BLEDeviceAttribute : String {
case peripheral, signalCharacteristic, payloadCharacteristic, payloadSharingCharacteristic, operatingSystem, payloadData, rssi, txPower
}
enum BLEDeviceOperatingSystem : String {
case android, ios, restored, unknown, shared
}
public typealias BLE_RSSI = Int
public typealias BLE_TxPower = Int
class BLEPseudoDeviceAddress {
let address: Int64
let data: Data
var description: String { get {
return "BLEPseudoDeviceAddress(address=\(address),data=\(data.base64EncodedString()))"
}}
init(value: Int64) {
data = BLEPseudoDeviceAddress.encode(value)
address = BLEPseudoDeviceAddress.decode(data)!
}
init?(data: Data) {
guard data.count == 6, let value = BLEPseudoDeviceAddress.decode(data) else {
return nil
}
address = value
self.data = BLEPseudoDeviceAddress.encode(address)
}
convenience init?(fromAdvertisementData: [String: Any]) {
guard let manufacturerData = fromAdvertisementData[CBAdvertisementDataManufacturerDataKey] as? Data else {
return nil
}
guard let manufacturerId = manufacturerData.uint16(0) else {
return nil
}
if manufacturerId == BLESensorConfiguration.BLEManufacturerId, manufacturerData.count == 8 {
self.init(data: Data(manufacturerData.subdata(in: 2..<8)))
}
else {
return nil
}
}
private static func encode(_ value: Int64) -> Data {
var data = Data()
data.append(value)
return Data(data.subdata(in: 2..<8))
}
private static func decode(_ data: Data) -> Int64? {
var decoded = Data(repeating: 0, count: 2)
decoded.append(data)
return decoded.int64(0)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/BLE/BLEReceiver.swift
================================================
//
// BLEReceiver.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreBluetooth
import os
/**
信标接收器扫描具有固定服务UUID的外围设备。
*/
protocol BLEReceiver : Sensor {
}
/**
信标接收器在前台和后台模式下扫描具有固定服务UUID的外围设备。Android设备的后台扫描是不需要特殊考虑的因为scanForPeripherals每次调用都会返回所有存在的Android设备。iOS后台扫描因后台模式下的复杂性,需要开放连接才能订阅用作触发器而将两个iOS设备保持在后台状态(而不是挂起或者被杀)的通知特性。对于iOS-iOS设备,一旦检测到,接收器将
(1)向发送器写入空白数据,从而触发发送器在8秒后发送特征数据更新,然后
(2)触发接收器接收更新通知
(3)以此为 readRSSI调用创建机会,并重复此循环过程,使两者保持唤醒。
请注意,iOS进程不可靠如果
(1)用户通过飞行模式设置关闭蓝牙,
(2)设备重新启动,
(3)如果应用程序被用户杀死,蓝牙状态将完全失去不能恢复。
*/
class SpecificBLEReceiver: NSObject, BLEReceiver, BLEDatabaseDelegate, CBCentralManagerDelegate, CBPeripheralDelegate {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "BLE.SpecificBLEReceiver")
private var delegates: [SensorDelegate] = []
/// 所有BLETransmitter和BLEReceiver任务的专用队列
private let queue: DispatchQueue!
private let delegateQueue: DispatchQueue
private let database: BLEDatabase
private let payloadDataSupplier: PayloadDataSupplier
private var central: CBCentralManager!
private let emptyData = Data(repeating: 0, count: 0)
private var scanTimer: DispatchSourceTimer?
private let scanTimerQueue = DispatchQueue(label: "Sensor.BLE.SpecificBLEReceiver.ScanTimer")
private let scheduleScanQueue = DispatchQueue(label: "Sensor.BLE.SpecificBLEReceiver.ScheduleScan")
/// 用于记录发现的设备的扫描结果队列,记录没有立即挂起的操作的设备。
private var scanResults: [BLEDevice] = []
///初始化BLEReceiver,该接收器与发送器共享相同的顺序调度队列,因为同时进行传输和接收操作影响蓝牙稳定性。接收器和发射器共享一个设备的公共数据库。
required init(queue: DispatchQueue, delegateQueue: DispatchQueue, database: BLEDatabase, payloadDataSupplier: PayloadDataSupplier) {
self.queue = queue
self.delegateQueue = delegateQueue
self.database = database
self.payloadDataSupplier = payloadDataSupplier
super.init()
if central == nil {
self.central = CBCentralManager(delegate: self, queue: queue, options: [
CBCentralManagerOptionRestoreIdentifierKey : "Sensor.BLE.SpecificBLEReceiver",
CBCentralManagerOptionShowPowerAlertKey : true]
)
}
database.add(delegate: self)
}
func add(delegate: SensorDelegate) {
delegates.append(delegate)
}
func start() {
logger.debug("start")
guard central != nil else {
return
}
if central.state == .poweredOn {
scan("start")
}
}
func stop() {
logger.debug("stop")
guard central != nil else {
return
}
guard central.isScanning else {
logger.fault("stop denied, already stopped")
return
}
scanTimer?.cancel()
scanTimer = nil
queue.async {
self.central.stopScan()
}
database.devices().forEach() { device in
if let peripheral = device.peripheral, peripheral.state != .disconnected {
disconnect("stop", peripheral)
}
}
}
// MARK:- 扫描外设并在需要时建立连接
/// 所有的工作都基于scan循环
func scan(_ source: String) {
logger.debug("scan (source=\(source)")
guard central.state == .poweredOn else {
logger.fault("scan failed, bluetooth is not powered on")
return
}
// 扫描广播Pioneer服务的外围设备。
// 这将找到所有Android和iOS前台广告
// 但它将错过iOS的后台广播,除非行程传感器已启用,屏幕暂时打开。
queue.async { self.taskScanForPeripherals() }
// 注册已经连接正在广播传感器服务的设备。
//它捕捉到在状态恢复时可能被CoreBooth遗漏或内部错误的孤立的设备。
queue.async { self.taskRegisterConnectedPeripherals() }
// 解析通过发送器获得的设备标识的外围设备。当iOS中心设备连接到此外围设备,发送器代码注册中心设备的地址作为新设备并挂至此处以建立对等连接。这可以启用检测另一个的设备(例如,屏幕打开时)的设备并触发两个设备以相互检测。
queue.async { self.taskResolveDevicePeripherals() }
//删除一段时间没有看到的设备,大约20分钟后标识符就会改变,因此,维护一个连接是浪费的。
queue.async { self.taskRemoveExpiredDevices() }
queue.async { self.taskRemoveDuplicatePeripherals() }
// iOS devices are kept in background state indefinitely
// (instead of dropping into suspended or terminated state)
// by a series of time delayed BLE operations. While this
// device is awake, it will write data to other iOS devices
// to keep them awake, and vice versa.
//iOS设备无限期地保持在后台状态(而不是进入暂停或终止状态)通过一系列延时的BLE操作。当这个设备处于唤醒状态,它将向其它iOS设备写入数据让他们保持唤醒,反之亦然。
queue.async { self.taskWakeTransmitters() }
queue.async { self.taskIosMultiplex() }
// 如果发现的设备有挂起的任务,将连接到该设备。绝大多数设备将立即连接发现,如果他们有一个挂起的任务(例如,建立其操作系统或读取其有效负载)。
// 但如果他们已经完全完成了任务,就不会有悬而未决的任务(例如,有操作系统、有效负载和最近的RSSI测量),它们被放入扫描结果队列中通过此连接任务进行定期检查(例如,如果RSSI现有值现在已过期)。
queue.async { self.taskConnect() }
scheduleScan("scan")
}
///在延迟8秒后安排信标扫描,以便状态从后台更改为挂起之前再次开始扫描。
private func scheduleScan(_ source: String) {
scheduleScanQueue.sync {
scanTimer?.cancel()
scanTimer = DispatchSource.makeTimerSource(queue: scanTimerQueue)
scanTimer?.schedule(deadline: DispatchTime.now() + BLESensorConfiguration.notificationDelay)
scanTimer?.setEventHandler { [weak self] in
self?.scan("scheduleScan|"+source)
}
scanTimer?.resume()
}
}
/**
扫描广播信标服务的外围设备。
*/
private func taskScanForPeripherals() {
// Scan for peripherals -> didDiscover
let scanForServices: [CBUUID] = [BLESensorConfiguration.serviceUUID]
central.scanForPeripherals(
withServices: scanForServices,
options: [CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [BLESensorConfiguration.serviceUUID]])
}
/**
注册所有连接的将Pioneer服务作为设备进行广播的外围设备。
*/
private func taskRegisterConnectedPeripherals() {
central.retrieveConnectedPeripherals(withServices: [BLESensorConfiguration.serviceUUID]).forEach() { peripheral in
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
let device = database.device(targetIdentifier)
if device.peripheral == nil || device.peripheral != peripheral {
logger.debug("taskRegisterConnectedPeripherals (device=\(device))")
_ = database.device(peripheral, delegate: self)
}
}
}
/**
解析所有数据库中设备的外围设备。这将启用对称连接功能。
*/
private func taskResolveDevicePeripherals() {
let devicesToResolve = database.devices().filter { $0.peripheral == nil }
devicesToResolve.forEach() { device in
guard let identifier = UUID(uuidString: device.identifier) else {
return
}
let peripherals = central.retrievePeripherals(withIdentifiers: [identifier])
if let peripheral = peripherals.last {
logger.debug("taskResolveDevicePeripherals (resolved=\(device))")
_ = database.device(peripheral, delegate: self)
}
}
}
/**
删除一段时间未更新的设备,因为UUID可能在超出范围超过20分钟后发生更改,因此需要重新发现。
*/
private func taskRemoveExpiredDevices() {
let devicesToRemove = database.devices().filter { Date().timeIntervalSince($0.lastUpdatedAt) > BLESensorConfiguration.TimeIntervalPeripheralClean }
devicesToRemove.forEach() { device in
logger.debug("taskRemoveExpiredDevices (remove=\(device))")
database.delete(device.identifier)
if let peripheral = device.peripheral {
disconnect("taskRemoveExpiredDevices", peripheral)
}
}
}
/**
删除具有相同有效负载数据但不同外围设备的设备。
*/
private func taskRemoveDuplicatePeripherals() {
var index: [PayloadData:BLEDevice] = [:]
let devices = database.devices()
devices.forEach() { device in
guard let payloadData = device.payloadData else {
return
}
guard let duplicate = index[payloadData] else {
index[payloadData] = device
return
}
var keeping = device
if device.peripheral != nil, duplicate.peripheral == nil {
keeping = device
} else if duplicate.peripheral != nil, device.peripheral == nil {
keeping = duplicate
} else if device.payloadDataLastUpdatedAt > duplicate.payloadDataLastUpdatedAt {
keeping = device
} else {
keeping = duplicate
}
let discarding = (keeping.identifier == device.identifier ? duplicate : device)
index[payloadData] = keeping
database.delete(discarding.identifier)
self.logger.debug("taskRemoveDuplicatePeripherals (payload=\(payloadData.shortName),device=\(device.identifier),duplicate=\(duplicate.identifier),keeping=\(keeping.identifier))")
}
}
/**
唤醒所有连接的iOS设备的Transmitter
*/
private func taskWakeTransmitters() {
database.devices().forEach() { device in
guard device.operatingSystem == .ios, let peripheral = device.peripheral, peripheral.state == .connected else {
return
}
guard device.timeIntervalSinceLastUpdate < TimeInterval.minute else {
connect("taskWakeTransmitters", peripheral)
return
}
wakeTransmitter("taskWakeTransmitters", device)
}
}
/**
连接到设备并保持并发连接最大配额
*/
private func taskConnect() {
let didDiscover = taskConnectScanResults()
let hasPendingTask = didDiscover.filter({ deviceHasPendingTask($0) })
let toBeRefreshed = database.devices().filter({ !hasPendingTask.contains($0) && $0.peripheral?.state == .connected })
let asymmetric = database.devices().filter({ !hasPendingTask.contains($0) && $0.operatingSystem == .unknown && $0.peripheral?.state != .connected })
hasPendingTask.forEach() { device in
guard let peripheral = device.peripheral else {
return
}
connect("taskConnect|hasPending", peripheral);
}
toBeRefreshed.forEach() { device in
guard let peripheral = device.peripheral else {
return
}
connect("taskConnect|refresh", peripheral);
}
asymmetric.forEach() { device in
guard let peripheral = device.peripheral else {
return
}
connect("taskConnect|asymmetric", peripheral);
}
}
/// 清空扫描结果以生成最近发现的设备列表,以便进行连接和处理
private func taskConnectScanResults() -> [BLEDevice] {
var set: Set = []
var list: [BLEDevice] = []
while let device = scanResults.popLast() {
if set.insert(device).inserted, let peripheral = device.peripheral, peripheral.state != .connected {
list.append(device)
logger.debug("taskConnectScanResults, didDiscover (device=\(device))")
}
}
return list
}
///检查设备是否有挂起的任务
private func deviceHasPendingTask(_ device: BLEDevice) -> Bool {
if device.operatingSystem == .unknown || device.operatingSystem == .restored {
return true
}
if device.payloadData == nil {
return true
}
if device.timeIntervalSinceLastPayloadDataUpdate > BLESensorConfiguration.TimeIntervalForPayloadDataUpdate {
return true
}
if device.operatingSystem == .ios, let peripheral = device.peripheral, peripheral.state != .connected {
return true
}
return false
}
/// 检查iOS设备是否正在等待连接,如果需要,检查可用容量
private func taskIosMultiplex() {
let devices = database.devices().filter({ $0.operatingSystem == .ios && $0.peripheral != nil })
let connected = devices.filter({ $0.peripheral?.state == .connected }).sorted(by: { $0.timeIntervalBetweenLastConnectedAndLastAdvert > $1.timeIntervalBetweenLastConnectedAndLastAdvert })
let pending = devices.filter({ $0.peripheral?.state != .connected }).sorted(by: { $0.lastConnectRequestedAt < $1.lastConnectRequestedAt })
logger.debug("taskIosMultiplex summary (connected=\(connected.count),pending=\(pending.count))")
connected.forEach() { device in
logger.debug("taskIosMultiplex, connected (device=\(device.description),upTime=\(device.timeIntervalBetweenLastConnectedAndLastAdvert))")
}
pending.forEach() { device in
logger.debug("taskIosMultiplex, pending (device=\(device.description),downTime=\(device.timeIntervalSinceLastConnectRequestedAt))")
}
// 如果有剩余容量,请重试所有挂起的连接
if connected.count < BLESensorConfiguration.maxConcurrentConnection {
pending.forEach() { device in
guard let toBeConnected = device.peripheral else {
return
}
connect("taskIosMultiplex|retry", toBeConnected);
}
}
// 达到容量时启动多路复用
guard connected.count > BLESensorConfiguration.maxConcurrentConnection, pending.count > 0, let deviceToBeDisconnected = connected.first, let peripheralToBeDisconnected = deviceToBeDisconnected.peripheral, deviceToBeDisconnected.timeIntervalBetweenLastConnectedAndLastAdvert > TimeInterval.minute else {
return
}
logger.debug("taskIosMultiplex, multiplexing (toBeDisconnected=\(deviceToBeDisconnected.description))")
disconnect("taskIosMultiplex", peripheralToBeDisconnected)
pending.forEach() { device in
guard let toBeConnected = device.peripheral else {
return
}
connect("taskIosMultiplex|multiplex", toBeConnected);
}
}
/// 根据当前状态和设备记录的可用信息在外围设备上启动下一个操作
private func taskNextAction(_ source: String, peripheral: CBPeripheral) {
let device = database.device(peripheral, delegate: self)
if device.rssi == nil {
// 1. RSSI
logger.debug("taskNextAction (goal=rssi,device=\(device))")
readRSSI("taskNextAction|" + source, peripheral)
} else if !(device.signalCharacteristic != nil && device.payloadCharacteristic != nil) {
// 2. Characteristics
logger.debug("taskNextAction (goal=characteristics,device=\(device))")
discoverServices("taskNextAction|" + source, peripheral)
} else if device.payloadData == nil {
// 3. Payload
logger.debug("taskNextAction (goal=payload,device=\(device))")
readPayload("taskNextAction|" + source, device)
} else if device.timeIntervalSinceLastPayloadDataUpdate > BLESensorConfiguration.TimeIntervalForPayloadDataUpdate {
// 4. Payload update
logger.debug("taskNextAction (goal=payloadUpdate,device=\(device),elapsed=\(device.timeIntervalSinceLastPayloadDataUpdate))")
readPayload("taskNextAction|" + source, device)
} else if device.operatingSystem != .ios {
// 5. Disconnect Android
logger.debug("taskNextAction (goal=disconnect|\(device.operatingSystem.rawValue),device=\(device))")
disconnect("taskNextAction|" + source, peripheral)
} else {
// 6. Scan
logger.debug("taskNextAction (goal=scan,device=\(device))")
scheduleScan("taskNextAction|" + source)
}
}
/**
连接外设。根据Apple文档的建议,在启动connect之前,扫描会暂时停止,否则挂起的扫描操作往往具有优先权,连接需要更长的时间才能启动。扫描计划稍后继续,以确保扫描恢复,即使连接失败。
*/
private func connect(_ source: String, _ peripheral: CBPeripheral) {
let device = database.device(peripheral, delegate: self)
logger.debug("connect (source=\(source),device=\(device))")
guard central.state == .poweredOn else {
logger.fault("connect denied, central not powered on (source=\(source),device=\(device))")
return
}
queue.async {
device.lastConnectRequestedAt = Date()
self.central.retrievePeripherals(withIdentifiers: [peripheral.identifier]).forEach {
if $0.state != .connected {
if let lastAttempt = device.lastConnectionInitiationAttempt {
if (Date() > lastAttempt + BLESensorConfiguration.toConnectionTimeout) {
self.logger.fault("connect, timeout forcing disconnect (source=\(source),device=\(device),elapsed=\(-lastAttempt.timeIntervalSinceNow))")
device.lastConnectionInitiationAttempt = nil
self.queue.async { self.central.cancelPeripheralConnection(peripheral) }
} else {
self.logger.debug("connect, retrying (source=\(source),device=\(device),elapsed=\(-lastAttempt.timeIntervalSinceNow))")
self.central.connect($0)
}
} else {
self.logger.debug("connect, initiation (source=\(source),device=\(device))")
device.lastConnectionInitiationAttempt = Date()
self.central.connect($0)
}
} else {
self.taskNextAction("connect|" + source, peripheral: $0)
}
}
}
scheduleScan("connect")
}
/**
断开外围设备。断开连接时,将为iOS设备发出连接请求,以保持开放的连接,Android没有进一步的行动。
*/
private func disconnect(_ source: String, _ peripheral: CBPeripheral) {
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
logger.debug("disconnect (source=\(source),peripheral=\(targetIdentifier))")
guard peripheral.state == .connected || peripheral.state == .connecting else {
logger.fault("disconnect denied, peripheral not connected or connecting (source=\(source),peripheral=\(targetIdentifier),state=\(peripheral.state))")
return
}
queue.async { self.central.cancelPeripheralConnection(peripheral) }
}
/// 读取RSSI
private func readRSSI(_ source: String, _ peripheral: CBPeripheral) {
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
logger.debug("readRSSI (source=\(source),peripheral=\(targetIdentifier))")
guard peripheral.state == .connected else {
logger.fault("readRSSI denied, peripheral not connected (source=\(source),peripheral=\(targetIdentifier))")
scheduleScan("readRSSI")
return
}
queue.async { peripheral.readRSSI() }
}
/// 搜索Pioneer服务
private func discoverServices(_ source: String, _ peripheral: CBPeripheral) {
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
logger.debug("discoverServices (source=\(source),peripheral=\(targetIdentifier))")
guard peripheral.state == .connected else {
logger.fault("discoverServices denied, peripheral not connected (source=\(source),peripheral=\(targetIdentifier))")
scheduleScan("discoverServices")
return
}
queue.async {
let services: [CBUUID] = [BLESensorConfiguration.serviceUUID]
peripheral.discoverServices(services)
}
}
/// 从设备中读取Payload
private func readPayload(_ source: String, _ device: BLEDevice) {
logger.debug("readPayload (source=\(source),peripheral=\(device.identifier))")
guard let peripheral = device.peripheral, peripheral.state == .connected else {
logger.fault("readPayload denied, peripheral not connected (source=\(source),peripheral=\(device.identifier))")
return
}
guard let payloadCharacteristic = device.payloadCharacteristic != nil ? device.payloadCharacteristic : nil else {
logger.fault("readPayload denied, device missing payload characteristic (source=\(source),peripheral=\(device.identifier))")
discoverServices("readPayload", peripheral)
return
}
let timeIntervalSinceLastReadPayloadRequestedAt = Date().timeIntervalSince(device.lastReadPayloadRequestedAt)
guard timeIntervalSinceLastReadPayloadRequestedAt > 2 else {
logger.fault("readPayload denied, duplicate request (source=\(source),peripheral=\(device.identifier),elapsed=\(timeIntervalSinceLastReadPayloadRequestedAt)")
return
}
// Initiate read payload
device.lastReadPayloadRequestedAt = Date()
if device.operatingSystem == .android, let peripheral = device.peripheral {
discoverServices("readPayload|android", peripheral)
} else {
queue.async { peripheral.readValue(for: payloadCharacteristic) }
}
}
/**
通过向信标特性写入空白数据唤醒发射机。这将触发发送器在8秒内生成数据值更新通知,这将触发此接收器接收didUpdateValueFor调用,以保持发射器和接收器处于唤醒状态,同时最大化蓝牙呼叫之间的时间间隔,以最大限度地降低功耗。
*/
private func wakeTransmitter(_ source: String, _ device: BLEDevice) {
guard device.operatingSystem == .ios, let peripheral = device.peripheral, let characteristic = device.signalCharacteristic else {
return
}
logger.debug("wakeTransmitter (source=\(source),peripheral=\(device.identifier),write=\(characteristic.properties.contains(.write))")
queue.async { peripheral.writeValue(self.emptyData, for: characteristic, type: .withResponse) }
}
// MARK:- BLEDatabaseDelegate
func bleDatabase(didCreate device: BLEDevice) {
// 特点:写时对称连接
// BLETransmitter中的所有CoreBluetooth代理回调都将注册与此外设交互的中心
// 并在这里生成一个didCreate回调来触发scan,其中包括一个用于解析所有
// 实际外设的设备标识符。
scheduleScan("bleDatabase:didCreate (device=\(device.identifier))")
}
// MARK:- CBCentralManagerDelegate
/// 恢复状态后恢复设备
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
logger.debug("willRestoreState")
self.central = central
central.delegate = self
if let restoredPeripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
for peripheral in restoredPeripherals {
let targetIdentifier = TargetIdentifier(peripheral: peripheral)
let device = database.device(peripheral, delegate: self)
if device.operatingSystem == .unknown {
device.operatingSystem = .restored
}
if peripheral.state == .connected {
device.lastConnectedAt = Date()
}
logger.debug("willRestoreState (peripheral=\(targetIdentifier))")
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// Bluetooth on -> Scan
if (central.state == .poweredOn) {
logger.debug("Update state (state=poweredOn))")
delegateQueue.async {
self.delegates.forEach({ $0.sensor(.BLE, didUpdateState: .on) })
}
scan("updateState")
} else {
logger.debug("Update state (state=\(central.state.description))")
delegateQueue.async {
self.delegates.forEach({ $0.sensor(.BLE, didUpdateState: .off) })
}
}
}
/// 在具有相同伪设备地址的设备之间共享有效负载数据
private func shareDataAcrossDevices(_ pseudoDeviceAddress: BLEPseudoDeviceAddress) {
let devicesWithSamePseudoAddress = database.devices().filter({ pseudoDeviceAddress.address == $0.pseudoDeviceAddress?.address && $0.timeIntervalSinceCreated <= BLESensorConfiguration.TimeIntervalForAndroidAdvertRefresh })
guard let mostRecentDevice = devicesWithSamePseudoAddress.filter({ $0.payloadData != nil }).sorted(by: { $0.payloadDataLastUpdatedAt > $1.payloadDataLastUpdatedAt }).first, let payloadData = mostRecentDevice.payloadData else {
return
}
let payloadDataLastUpdatedAt = mostRecentDevice.payloadDataLastUpdatedAt
let devicesToCopyPayload = devicesWithSamePseudoAddress.filter({ $0.payloadData == nil })
devicesToCopyPayload.forEach({
$0.signalCharacteristic = mostRecentDevice.signalCharacteristic
$0.payloadCharacteristic = mostRecentDevice.payloadCharacteristic
$0.operatingSystem = .android
$0.payloadData = payloadData
$0.payloadDataLastUpdatedAt = payloadDataLastUpdatedAt
logger.debug("shareDataAcrossDevices, copied payload data (from=\(mostRecentDevice.description),to=\($0.description))")
})
let devicesWithSamePayload = database.devices().filter({ payloadData == $0.payloadData })
let devicesToCopyAddress = devicesWithSamePayload.filter({ $0.pseudoDeviceAddress == nil })
devicesToCopyAddress.forEach({
$0.pseudoDeviceAddress = pseudoDeviceAddress
logger.debug("shareDataAcrossDevices, copied pseudo address (payloadData=\(payloadData.shortName),to=\($0.description))")
})
}
/// 设备发现将触发连接以解析操作系统并读取iOS和Android设备的有效负载。
/// 对于正在进行的RSSI测量,iOS设备的连接保持活动状态,对于Android设备的连接保持关闭状态,因为
/// iOS设备可以依赖此发现回调(由常规扫描调用触发)来提供持续的RSSI和TX电源更新,从而消除了对Android设备保持连接打开的需要,这对Android设备来说可能会导致系统的稳定性问题。
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let device = database.device(peripheral, delegate: self)
device.lastDiscoveredAt = Date()
device.rssi = BLE_RSSI(RSSI.intValue)
if let pseudoDeviceAddress = BLEPseudoDeviceAddress(fromAdvertisementData: advertisementData) {
device.pseudoDeviceAddress = pseudoDeviceAddress
shareDataAcrossDevices(pseudoDeviceAddress)
}
if let txPower = (advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber)?.intValue {
device.txPower = BLE_TxPower(txPower)
}
logger.debug("didDiscover (device=\(device),rssi=\((String(describing: device.rssi))),txPower=\((String(describing: device.txPower))))")
if deviceHasPendingTask(device) {
connect("didDiscover", peripheral);
} else {
scanResults.append(device)
}
scheduleScan("didDiscover")
}
/// 成功连接到设备将启动下一个挂起的操作。
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
// connect -> readRSSI -> discoverServices
let device = database.device(peripheral, delegate: self)
device.lastConnectedAt = Date()
logger.debug("didConnect (device=\(device))")
taskNextAction("didConnect", peripheral: peripheral)
}
/// 未能连接到设备将导致无效设备取消注册或以其他方式尝试重新连接。
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
let device = database.device(peripheral, delegate: self)
logger.debug("didFailToConnect (device=\(device),error=\(String(describing: error)))")
if String(describing: error).contains("Device is invalid") {
logger.debug("Unregister invalid device (device=\(device))")
database.delete(device.identifier)
} else {
connect("didFailToConnect", peripheral)
}
}
///正常断开通常是由于设备超出范围或设备更改标识而导致的,因此会启动重新连接调用
///在这里,iOS设备可以在可能的情况下恢复连接。这对于Android设备来说是不必要的,因为它们可以被用户重新发现
///常规扫描。请注意,重新连接到iOS设备可能会在长时间超出范围后失败
///目标设备可能在大约20分钟后改变了身份。这需要重新发现,如果iOS设备
///处于后台状态,因此需要启用位置和屏幕打开来触发重新发现。
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
// Disconnected -> Connect if iOS
let device = database.device(peripheral, delegate: self)
device.lastDisconnectedAt = Date()
logger.debug("didDisconnectPeripheral (device=\(device),error=\(String(describing: error)))")
if device.operatingSystem == .ios {
device.signalCharacteristic = nil
device.payloadCharacteristic = nil
connect("didDisconnectPeripheral", peripheral)
}
}
// MARK: - CBPeripheralDelegate
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
// Read RSSI -> Read Code | Notify delegates -> Scan again
// scan -> wakeTransmitter -> didUpdateValueFor -> readRSSI -> notifyDelegates -> scheduleScan -> scan
let device = database.device(peripheral, delegate: self)
device.rssi = BLE_RSSI(RSSI.intValue)
logger.debug("didReadRSSI (device=\(device),rssi=\(String(describing: device.rssi)),error=\(String(describing: error)))")
taskNextAction("didReadRSSI", peripheral: peripheral)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
// Discover services -> Discover characteristics | Disconnect
let device = database.device(peripheral, delegate: self)
logger.debug("didDiscoverServices (device=\(device),error=\(String(describing: error)))")
guard let services = peripheral.services else {
disconnect("didDiscoverServices|serviceEmpty", peripheral)
return
}
for service in services {
if service.uuid == BLESensorConfiguration.serviceUUID {
logger.debug("didDiscoverServices, found sensor service (device=\(device))")
queue.async { peripheral.discoverCharacteristics(nil, for: service) }
return
}
}
disconnect("didDiscoverServices|serviceNotFound", peripheral)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
// Discover characteristics -> Notify delegates -> Disconnect | Wake transmitter -> Scan again
let device = database.device(peripheral, delegate: self)
logger.debug("didDiscoverCharacteristicsFor (device=\(device),error=\(String(describing: error)))")
guard let characteristics = service.characteristics else {
disconnect("didDiscoverCharacteristicsFor|characteristicEmpty", peripheral)
return
}
for characteristic in characteristics {
switch characteristic.uuid {
case BLESensorConfiguration.androidSignalCharacteristicUUID:
device.operatingSystem = .android
device.signalCharacteristic = characteristic
logger.debug("didDiscoverCharacteristicsFor, found android signal characteristic (device=\(device))")
case BLESensorConfiguration.iosSignalCharacteristicUUID:
let notify = characteristic.properties.contains(.notify)
let write = characteristic.properties.contains(.write)
device.operatingSystem = .ios
device.signalCharacteristic = characteristic
queue.async { peripheral.setNotifyValue(true, for: characteristic) }
logger.debug("didDiscoverCharacteristicsFor, found ios signal characteristic (device=\(device),notify=\(notify),write=\(write))")
case BLESensorConfiguration.payloadCharacteristicUUID:
device.payloadCharacteristic = characteristic
logger.debug("didDiscoverCharacteristicsFor, found payload characteristic (device=\(device))")
default:
logger.fault("didDiscoverCharacteristicsFor, found unknown characteristic (device=\(device),characteristic=\(characteristic.uuid))")
}
}
if device.operatingSystem == .android, let payloadCharacteristic = device.payloadCharacteristic {
if device.payloadData == nil || device.timeIntervalSinceLastPayloadDataUpdate > BLESensorConfiguration.TimeIntervalForPayloadDataUpdate {
queue.async { peripheral.readValue(for: payloadCharacteristic) }
} else {
disconnect("didDiscoverCharacteristicsFor|android", peripheral)
}
}
scheduleScan("didDiscoverCharacteristicsFor")
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
// Wrote characteristic -> Scan again
let device = database.device(peripheral, delegate: self)
logger.debug("didWriteValueFor (device=\(device),error=\(String(describing: error)))")
scheduleScan("didWriteValueFor")
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
// iOS only
// Modified service -> Invalidate beacon -> Scan
let device = database.device(peripheral, delegate: self)
let characteristics = invalidatedServices.map { $0.characteristics }.count
logger.debug("didModifyServices (device=\(device),characteristics=\(characteristics))")
guard characteristics == 0 else {
return
}
device.signalCharacteristic = nil
device.payloadCharacteristic = nil
if peripheral.state == .connected {
discoverServices("didModifyServices", peripheral)
} else if peripheral.state != .connecting {
connect("didModifyServices", peripheral)
}
}
/// All read characteristic requests will trigger this call back to handle the response.
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
// Updated value -> Read RSSI | Read Payload
// Beacon characteristic is writable, primarily to enable non-transmitting Android devices to submit their
// beacon code and RSSI as data to the transmitter via GATT write. The characteristic is also notifying on
// iOS devices, to offer a mechanism for waking receivers. The process works as follows, (1) receiver writes
// blank data to transmitter, (2) transmitter broadcasts value update notification after 8 seconds, (3)
// receiver is woken up to handle didUpdateValueFor notification, (4) receiver calls readRSSI, (5) readRSSI
// call completes and schedules scan after 8 seconds, (6) scan writes blank data to all iOS transmitters.
// Process repeats to keep both iOS transmitters and receivers awake while maximising time interval between
// bluetooth calls to minimise power usage.
let timestamp = Date()
let device = database.device(peripheral, delegate: self)
logger.debug("didUpdateValueFor (device=\(device),characteristic=\(characteristic.uuid),error=\(String(describing: error)))")
switch characteristic.uuid {
case BLESensorConfiguration.iosSignalCharacteristicUUID:
logger.debug("didUpdateValueFor (device=\(device),characteristic=iosSignalCharacteristic,error=\(String(describing: error)))")
device.lastNotifiedAt = Date()
readRSSI("didUpdateValueFor", peripheral)
return
case BLESensorConfiguration.androidSignalCharacteristicUUID:
logger.fault("didUpdateValueFor (device=\(device),characteristic=androidSignalCharacteristic,error=\(String(describing: error)))")
case BLESensorConfiguration.payloadCharacteristicUUID:
// Read payload data
logger.debug("didUpdateValueFor (device=\(device),characteristic=payloadCharacteristic,error=\(String(describing: error)))")
if let data = characteristic.value {
guard PayloadData(data).isValid(timestamp:timestamp) else{
return
}
device.payloadData = PayloadData(data)
}
if device.operatingSystem == .android {
disconnect("didUpdateValueFor|payload|android", peripheral)
}
default:
logger.fault("didUpdateValueFor, unknown characteristic (device=\(device),characteristic=\(characteristic.uuid),error=\(String(describing: error)))")
}
scheduleScan("didUpdateValueFor")
return
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/BLE/BLESensor.swift
================================================
//
// BLESensor.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreBluetooth
protocol BLESensor : Sensor {
/**
BLE位置传感器基于CoreBluetooth,这是Pioneer系统用户交互基于的最主要的部分,几乎所有的工作都是围绕低功耗蓝牙传感器而展开的。
使用该功能需要蓝牙权限并申请后台权限
Requires : Signing & Capabilities : BackgroundModes : Uses Bluetooth LE accessories = YES
Requires : Signing & Capabilities : BackgroundModes : Acts as a Bluetooth LE accessory = YES
同时使用该功能需要隐私权限请求
Requires : Info.plist : Privacy - Bluetooth Always Usage Description
Requires : Info.plist : Privacy - Bluetooth Peripheral Usage Description
*/
}
// 定义BLE传感器配置数据
public struct BLESensorConfiguration {
// MARK:-BLE服务和特性UUID及制造商ID
//Pioneer系统的专用信标服务UUID,只要运行Pioneer系统的设备均会广播此服务,以被其他运行Pioneer系统的设备所识别
public static let serviceUUID: CBUUID = CBUUID(string: "9ea51088-5add-438b-a269-dd6289844034")
//用于控制外围设备和中心设备之间的连接的信令特性,主要用于识别设备上运行的系统
//Android设备对应的信令特征
public static let androidSignalCharacteristicUUID: CBUUID = CBUUID(string: "0ff27076-1fb2-4551-9670-7b468af8a9e7")
//iOS设备对应的信令特征
public static let iosSignalCharacteristicUUID: CBUUID = CBUUID(string: "64451d23-b364-4967-bb43-51b82a56f3a5")
//用于控制设备之间Payload(读取)的信令特性,只有当Pioneer专用信标服务下有该特征时,Payload才允许被其他设备读取
public static let payloadCharacteristicUUID: CBUUID = CBUUID(string: "2b5362a6-c995-4ab7-b744-cb366d4785bd")
//Android设备需要使用制造商ID来存储伪设备地址以屏蔽某些Android设备专用ID的过快更新
public static let BLEManufacturerId: UInt16 = UInt16(65530)
// MARK:- BLE信号特征动作码
//信令特征动作码用于写在交互数据前,作为交互信息的首部
//写入有效负载信令特征动作码
public static let signalCharacteristicActionForPayload: UInt8 = UInt8(1)
//写入RSSI信令特征动作码
public static let signalCharacteristicActionForRSSI: UInt8 = UInt8(2)
//写入共享有效负载信令特征动作码
public static let signalCharacteristicActionForPayloadSharing: UInt8 = UInt8(3)
// MARK:- 可配置的应用程序功能
public static let logLevel: SensorLoggerLevel = .debug
///设置为nil以禁用传感器,设置为distance(SpecificMobilitySensor.minimumResolution)启用传感器以给定的分辨率进行行程行性感应。
///另外,启用位置行程性传感器并结合iOS的屏幕唤醒可以使iOS设备之间直接交互,而非必需通过Android设备作为中继
public static var TravelSensorEnabled: Distance? = nil
///Payload定期更新时间间隔。设置后立即生效,无需重新启动BLESensor,也可在BLESensor激活时应用。
///设置为.never以禁用此功能
public static var TimeIntervalForPayloadDataUpdate: TimeInterval = TimeInterval.never
///Payload过滤时间间隔
///设置.never以禁用此功能
///设置时间间隔N以过滤在过去N秒内看到的重复的有效负载数据
public static var TimeIntervalForFilterDuplicatePayloadData: TimeInterval = TimeInterval.never
/// 删除一段时间未更新的外围设备记录。
/// Pioneer旨在保持与所有外设的短暂连接,以便记录所有接近的外设的接近度和接触持续时间。因此Pioneer会频繁地进行数据采样,为了减少工作量需要定期删除长时间未更新的外围设备(这些设备可能已经超出范围或者由于其他原因停止了Pioneer工作)记录以终止数据采样,而不是一直尝试与这些设备进行连接。
///当然,不管是ios设备还是Android设备,基于蓝牙的底层协议,设备的蓝牙地址会定期轮换,当某设备地址超时后显然Pioneer不必再为此连接做努力并记录,因为此地址已失效也就是短期内基本不会再探测到该设备地址。因此TimeIntervalPeripheralClean的设置必须合理,建议将其设置在Android设备扫描周期(大约2分钟)和ios蓝牙地址轮换周期之间。
public static var TimeIntervalPeripheralClean: TimeInterval = TimeInterval.minute * 2
// MARK:- BLE事件计时
//订阅通知之间的延迟
public static var notificationDelay: DispatchTimeInterval = DispatchTimeInterval.seconds(2)
//广播重置时间延迟
public static var TimeIntervalForAdvertRestart: TimeInterval = TimeInterval.hour
//并发连接的最大设备数目
public static var maxConcurrentConnection: Int = 12
//Android设备的广播刷新时间间隔
public static var TimeIntervalForAndroidAdvertRefresh: TimeInterval = TimeInterval.minute * 15
//连接超时时间间隔
public static var toConnectionTimeout: TimeInterval = TimeInterval(10)
}
class SpecificBLESensor : NSObject, BLESensor, BLEDatabaseDelegate {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "BLE.SpecificBLESensor")
private let sensorQueue = DispatchQueue(label: "Sensor.SpecificBLESensor.SensorQueue")
private let delegateQueue = DispatchQueue(label: "Sensor.SpecificLESensor.DelegateQueue")
private var delegates: [SensorDelegate] = []
private let database: BLEDatabase
private let transmitter: BLETransmitter
private let receiver: BLEReceiver
private var didReadPayloadData: [PayloadData:Date] = [:]
init(_ payloadDataSupplier: PayloadDataSupplier) {
database = SpecificBLEDatabase()
transmitter = SpecificBLETransmitter(queue: sensorQueue, delegateQueue: delegateQueue, database: database, payloadDataSupplier: payloadDataSupplier)
receiver = SpecificBLEReceiver(queue: sensorQueue, delegateQueue: delegateQueue, database: database, payloadDataSupplier: payloadDataSupplier)
super.init()
database.add(delegate: self)
}
func start() {
logger.debug("start")
receiver.start()
}
func stop() {
logger.debug("stop")
transmitter.stop()
receiver.stop()
}
func add(delegate: SensorDelegate) {
delegates.append(delegate)
transmitter.add(delegate: delegate)
receiver.add(delegate: delegate)
}
// MARK:- BLEDatabaseDelegate
func bleDatabase(didCreate device: BLEDevice) {
logger.debug("didDetect (device=\(device.identifier),payloadData=\(device.payloadData?.shortName ?? "nil"))")
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didDetect: device.identifier) }
}
}
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute) {
switch attribute {
case .rssi:
guard let rssi = device.rssi else {
return
}
let proximity = Proximity(unit: .RSSI, value: Double(rssi), calibration: device.calibration)
logger.debug("didMeasure (device=\(device.identifier),payloadData=\(device.payloadData?.shortName ?? "nil"),proximity=\(proximity.description))")
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didMeasure: proximity, fromTarget: device.identifier) }
}
guard let payloadData = device.payloadData else {
return
}
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didMeasure: proximity, fromTarget: device.identifier, withPayload: payloadData) }
}
case .payloadData:
guard let payloadData = device.payloadData else {
return
}
if BLESensorConfiguration.TimeIntervalForFilterDuplicatePayloadData != .never {
let removePayloadDataBefore = Date() - BLESensorConfiguration.TimeIntervalForFilterDuplicatePayloadData
let recentDidReadPayloadData = didReadPayloadData.filter({ $0.value >= removePayloadDataBefore })
didReadPayloadData = recentDidReadPayloadData
if let lastReportedAt = didReadPayloadData[payloadData] {
logger.debug("didRead, filtered duplicate (device=\(device.identifier),payloadData=\(payloadData.shortName),lastReportedAt=\(lastReportedAt.description))")
return
}
didReadPayloadData[payloadData] = Date()
}
logger.debug("didRead (device=\(device.identifier),payloadData=\(payloadData.shortName))")
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didRead: payloadData, fromTarget: device.identifier) }
}
default:
return
}
}
}
extension TargetIdentifier {
init(peripheral: CBPeripheral) {
self.init(peripheral.identifier.uuidString)
}
init(central: CBCentral) {
self.init(central.identifier.uuidString)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/BLE/BLETransmitter.swift
================================================
//
// BLETransmitter.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreBluetooth
/**
信标发送器广播一个固定的服务UUID以启用iOS的后台扫描。当iOS进入后台模式,UUID将从广播中消失,因此Android设备需要先搜索Apple设备,然后连接并发现用于读取UUID的服务。
*/
protocol BLETransmitter : Sensor {
}
/**
BLETransmitter的主要功能有两个:
1.保持iOS设备之间连接的信令特征,并使没有发送功能的Android设备(仅接收,像三星J6一样)通过将信标代码和RSSI作为数据写入该特征,来使其他Pioneer设备知道它们的存在。
2.发布信标标识数据的有效负载特征
保持Transmitter和Receiver在iOS后台模式下工作是一项重大挑战,尤其是当两个iOS设备都处于后台模式时。iOS上的Transmitter提供一个可被写入数据而触发的通知信标特性。当特征写入时,Transmitter将在8秒后调用updateValue通知Receiver,用didUpdateValueFor唤醒Receiver。这个过程可以循环重复在Trabsmitter和Receiver之间以保持两个设备处于唤醒状态。这对于可以完全依赖scanForPeripherals进行检测的Android通信是不必要的,Android - iOS和iOS -Android 检测同样。
基于唤醒的通知方法依赖于一个开放的连接,这对于iOS来说似乎很好,但是可能会导致Android的问题。实验发现,Android设备不能接受新的连接(没有显式连接断开)并且蓝牙堆栈在大约500个打开的连接后停止工作。设备需要重新启动才能恢复。但是,如果每个连接都断开,蓝牙协议栈就可以工作无限期,但频繁的连接和断开仍然会导致相同的问题。建议是
(1)工作完成后,请务必断开与Android的连接
(2)尽量减少与Android的连接数
(3)最大化连接之间的时间间隔。
考虑到这些,Transmitter在Android上不支持这种通知模式,而且仅仅在第一次接触时才执行连接以获取有效负载。
*/
class SpecificBLETransmitter : NSObject, BLETransmitter, CBPeripheralManagerDelegate {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "BLE.SpecificBLETransmitter")
private var delegates: [SensorDelegate] = []
/// 所有BLETransmitter和BLEReceiver任务的专用队列
private let queue: DispatchQueue
private let delegateQueue: DispatchQueue
private let database: BLEDatabase
private let payloadDataSupplier: PayloadDataSupplier
/// 用于管理所有连接的外围设备管理器,为简单起见,使用单个管理器。
private var peripheral: CBPeripheralManager!
/// BLETransmitter正在广播的信标服务和特性
private var signalCharacteristic: CBMutableCharacteristic?
private var payloadCharacteristic: CBMutableCharacteristic?
private var advertisingStartedAt: Date = Date.distantPast
/// 用于写入接收器以触发状态恢复或从挂起状态恢复到背景状态的伪数据。
private let emptyData = Data(repeating: 0, count: 0)
private var notifyTimer: DispatchSourceTimer?
private let notifyTimerQueue = DispatchQueue(label: "Sensor.BLE.SpecificBLETransmitter.Timer")
///初始化BLETransmitter,该BLETransmitter使用与BLEReceiver相同的调度队列。启用蓝牙时,Transmitter自动启动。
init(queue: DispatchQueue, delegateQueue: DispatchQueue, database: BLEDatabase, payloadDataSupplier: PayloadDataSupplier) {
self.queue = queue
self.delegateQueue = delegateQueue
self.database = database
self.payloadDataSupplier = payloadDataSupplier
super.init()
// 创建支持状态恢复的外围设备
if peripheral == nil {
//创建peripheral成功将调用代理对象的peripheralManagerDidUpdateState
self.peripheral = CBPeripheralManager(delegate: self, queue: queue, options: [
CBPeripheralManagerOptionRestoreIdentifierKey : "Sensor.BkLE.SpecificBLETransmitter",
// 如果在蓝牙关闭时打开应用程序,将此设置为false可阻止iOS显示警报。
CBPeripheralManagerOptionShowPowerAlertKey : true
])
}
}
func add(delegate: SensorDelegate) {
delegates.append(delegate)
}
func start() {
logger.debug("start")
guard peripheral != nil else {
return
}
guard peripheral.state == .poweredOn else {
logger.fault("start denied, not powered on")
return
}
if signalCharacteristic != nil, payloadCharacteristic != nil {
logger.debug("starting advert with existing characteristics")
if !peripheral.isAdvertising {
startAdvertising(withNewCharacteristics: false)
} else {
queue.async {
self.peripheral.stopAdvertising()
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
}
}
logger.debug("start successful, for existing characteristics")
} else {
startAdvertising(withNewCharacteristics: true)
logger.debug("start successful, for new characteristics")
}
signalCharacteristic?.subscribedCentrals?.forEach() { central in
_ = database.device(central.identifier.uuidString)
}
notifySubscribers("start")
}
func stop() {
logger.debug("stop")
guard peripheral != nil else {
return
}
guard peripheral.isAdvertising else {
logger.fault("stop denied, already stopped (source=%s)")
return
}
stopAdvertising()
}
private func startAdvertising(withNewCharacteristics: Bool) {
logger.debug("startAdvertising (withNewCharacteristics=\(withNewCharacteristics))")
if withNewCharacteristics || signalCharacteristic == nil || payloadCharacteristic == nil {
signalCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.iosSignalCharacteristicUUID, properties: [.write, .notify], value: nil, permissions: [.writeable])
payloadCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.payloadCharacteristicUUID, properties: [.read], value: nil, permissions: [.readable])
}
let service = CBMutableService(type: BLESensorConfiguration.serviceUUID, primary: true)
signalCharacteristic?.value = nil
payloadCharacteristic?.value = nil
service.characteristics = [signalCharacteristic!, payloadCharacteristic!]
queue.async {
self.peripheral.stopAdvertising()
self.peripheral.removeAllServices()
self.peripheral.add(service)
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
}
}
private func stopAdvertising() {
logger.debug("stopAdvertising()")
queue.async {
self.peripheral.stopAdvertising()
}
notifyTimer?.cancel()
notifyTimer = nil
}
/// 所有工作都从通知订阅者循环开始。
/// 在8秒后生成updateValue通知,通知所有订阅用户并保持iOS接收器处于唤醒状态。
private func notifySubscribers(_ source: String) {
notifyTimer?.cancel()
notifyTimer = DispatchSource.makeTimerSource(queue: notifyTimerQueue)
notifyTimer?.schedule(deadline: DispatchTime.now() + BLESensorConfiguration.notificationDelay)
notifyTimer?.setEventHandler { [weak self] in
guard let s = self, let logger = self?.logger, let signalCharacteristic = self?.signalCharacteristic else {
return
}
s.queue.async {
logger.debug("notifySubscribers (source=\(source))")
s.peripheral.updateValue(s.emptyData, for: signalCharacteristic, onSubscribedCentrals: nil)
}
let advertUpTime = Date().timeIntervalSince(s.advertisingStartedAt)
if s.peripheral.isAdvertising, advertUpTime > BLESensorConfiguration.TimeIntervalForAdvertRestart {
logger.debug("advertRestart (upTime=\(advertUpTime))")
s.startAdvertising(withNewCharacteristics: true)
}
}
notifyTimer?.resume()
}
// MARK:- CBPeripheralManagerDelegate
/// 恢复广播并恢复广播的特征。
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) {
logger.debug("willRestoreState")
self.peripheral = peripheral
peripheral.delegate = self
if let services = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] {
for service in services {
logger.debug("willRestoreState (service=\(service.uuid.uuidString))")
if let characteristics = service.characteristics {
for characteristic in characteristics {
logger.debug("willRestoreState (characteristic=\(characteristic.uuid.uuidString))")
switch characteristic.uuid {
case BLESensorConfiguration.androidSignalCharacteristicUUID:
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
signalCharacteristic = mutableCharacteristic
logger.debug("willRestoreState (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
} else {
logger.fault("willRestoreState characteristic not mutable (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
}
case BLESensorConfiguration.iosSignalCharacteristicUUID:
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
signalCharacteristic = mutableCharacteristic
logger.debug("willRestoreState (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
} else {
logger.fault("willRestoreState characteristic not mutable (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
}
case BLESensorConfiguration.payloadCharacteristicUUID:
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
payloadCharacteristic = mutableCharacteristic
logger.debug("willRestoreState (payloadCharacteristic=\(characteristic.uuid.uuidString))")
} else {
logger.fault("willRestoreState characteristic not mutable (payloadCharacteristic=\(characteristic.uuid.uuidString))")
}
default:
logger.debug("willRestoreState (unknownCharacteristic=\(characteristic.uuid.uuidString))")
}
}
}
}
}
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
// Bluetooth on -> Advertise
if (peripheral.state == .poweredOn) {
logger.debug("Update state (state=poweredOn)")
start()
} else {
logger.debug("Update state (state=\(peripheral.state.description))")
}
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
logger.debug("peripheralManagerDidStartAdvertising (error=\(String(describing: error)))")
if error == nil {
advertisingStartedAt = Date()
}
}
/**
写请求提供了一种机制,使不可使用发送器的设备(例如,三星J6只能接收)能够通过提交信标码和RSSI作为数据来知道它的存在。这也为iOS提供了一种向Transmitter写入空白数据,使其从挂起状态恢复到后台状态以增加了它在后台长时间内扫描的时间而不被杀死。有效负载共享也基于写特性,使Android对等机充当共享iOS设备有效负载的桥梁,从而iOS-iOS背景检测无需位置许可或屏幕打开,只需要后台探测和跟踪的方法。
*/
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
// Write -> Notify delegates -> Write response -> Notify subscribers
let timstamp = Date()
for request in requests {
let targetIdentifier = TargetIdentifier(central: request.central)
let targetDevice = database.device(targetIdentifier)
logger.debug("didReceiveWrite (central=\(targetIdentifier))")
if let data = request.value {
if data.count == 0 {
// 接收机在检测到发射机时写入空白数据,使iOS发射机从挂起状态恢复
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=wakeTransmitter)")
queue.async { peripheral.respond(to: request, withResult: .success) }
} else if let actionCode = data.uint8(0) {
switch actionCode {
case BLESensorConfiguration.signalCharacteristicActionForPayload:
// 只接收写有有效负载的Android设备,以使其存在被了解
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayload)")
// writePayload data 格式
// 0-0 : actionCode
// 1-2 : payload data count in bytes (UInt16)
// 3.. : payload data
if let payloadDataCount = data.uint16(1) {
logger.debug("didReceiveWrite -> didDetect=\(targetIdentifier)")
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didDetect: targetIdentifier) }
}
if data.count == (3 + payloadDataCount) {
let payloadData = PayloadData(data.subdata(in: 3.. didRead=\(payloadData.shortName),fromTarget=\(targetIdentifier)")
return
}
logger.debug("didReceiveWrite -> didRead=\(payloadData.shortName),fromTarget=\(targetIdentifier)")
queue.async { peripheral.respond(to: request, withResult: .success) }
targetDevice.operatingSystem = .android
targetDevice.receiveOnly = true
targetDevice.payloadData = payloadData
} else {
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayload)")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
} else {
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayload)")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
case BLESensorConfiguration.signalCharacteristicActionForRSSI:
//仅支持接收的Android设备写入起RSSI来使使其他设备知道该设备的接近程度
// Receive-only Android device writing its RSSI to make its proximity known
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writeRSSI)")
// writeRSSI data 格式
// 0-0 : actionCode
// 1-2 : rssi value (Int16)
if let rssi = data.int16(1) {
let proximity = Proximity(unit: .RSSI, value: Double(rssi), calibration: targetDevice.calibration)
logger.debug("didReceiveWrite -> didMeasure=\(proximity.description),fromTarget=\(targetIdentifier)")
queue.async { peripheral.respond(to: request, withResult: .success) }
targetDevice.operatingSystem = .android
targetDevice.receiveOnly = true
targetDevice.rssi = BLE_RSSI(rssi)
} else {
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writeRSSI)")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
case BLESensorConfiguration.signalCharacteristicActionForPayloadSharing:
// Android设备与此iOS设备共享检测到的iOS设备以达到后台检测的效果
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayloadSharing)")
// writePayloadSharing data 格式
// 0-0 : actionCode
// 1-2 : rssi value (Int16)
// 3-4 : payload sharing data count in bytes (UInt16)
// 5.. : payload sharing data (to be parsed by payload data supplier)
if let rssi = data.int16(1), let payloadDataCount = data.uint16(3) {
if data.count == (5 + payloadDataCount) {
let payloadSharingData = payloadDataSupplier.payload(data.subdata(in: 5.. didShare=\(payloadSharingData.description),fromTarget=\(targetIdentifier)")
queue.async { peripheral.respond(to: request, withResult: .success) }
delegateQueue.async {
self.delegates.forEach { $0.sensor(.BLE, didShare: payloadSharingData, fromTarget: targetIdentifier) }
}
targetDevice.operatingSystem = .android
targetDevice.rssi = BLE_RSSI(rssi)
payloadSharingData.forEach() { payloadData in
let sharedDevice = database.device(payloadData)
if sharedDevice.operatingSystem == .unknown {
sharedDevice.operatingSystem = .shared
}
sharedDevice.rssi = BLE_RSSI(rssi)
}
} else {
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayloadSharing)")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
} else {
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayloadSharing)")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
default:
logger.fault("didReceiveWrite (central=\(targetIdentifier),action=unknown,actionCode=\(actionCode))")
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
}
} else {
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
}
}
notifySubscribers("didReceiveWrite")
}
/// 来自中央设备的读取请求,用于从该外设获取有效负载数据。
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
// Read -> Notify subscribers
let central = database.device(TargetIdentifier(request.central.identifier.uuidString))
switch request.characteristic.uuid {
case BLESensorConfiguration.payloadCharacteristicUUID:
logger.debug("Read (central=\(central.description),characteristic=payload,offset=\(request.offset))")
let payloadDataSupplied = payloadDataSupplier.payload(PayloadTimestamp(), device: central)
guard let payloadData = payloadDataSupplied else {
logger.fault("Read, no payload data supplied (central=\(central.description),characteristic=payload,offset=\(request.offset),data=BLANK)")
queue.async { peripheral.respond(to: request, withResult: .invalidOffset) }
return
}
guard request.offset < payloadData.count else {
logger.fault("Read, invalid offset (central=\(central.description),characteristic=payload,offset=\(request.offset),data=\(payloadData.count))")
queue.async { peripheral.respond(to: request, withResult: .invalidOffset) }
return
}
request.value = (request.offset == 0 ? payloadData.data : payloadData.subdata(in: request.offset.. Notify subscribers
// iOS接收器订阅第一次接触时的信号特征。这样可以确保Transmitter和Receiver在第一次更新时被唤醒,而未来的循环将依赖didReceiveWrite作为触发器。
logger.debug("Subscribe (central=\(central.identifier.uuidString))")
_ = database.device(central.identifier.uuidString)
notifySubscribers("didSubscribeTo")
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
// Unsubscribe -> Notify subscribers
logger.debug("Unsubscribe (central=\(central.identifier.uuidString))")
_ = database.device(central.identifier.uuidString)
notifySubscribers("didUnsubscribeFrom")
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/BLE/BLEUtilities.swift
================================================
//
// BLEUtilities.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreBluetooth
@available(iOS 10.0, *)
extension CBManagerState: CustomStringConvertible {
public var description: String {
switch self {
case .poweredOff: return ".poweredOff"
case .poweredOn: return ".poweredOn"
case .resetting: return ".resetting"
case .unauthorized: return ".unauthorized"
case .unknown: return ".unknown"
case .unsupported: return ".unsupported"
@unknown default: return "undefined"
}
}
}
extension CBPeripheralState: CustomStringConvertible {
public var description: String {
switch self {
case .connected: return ".connected"
case .connecting: return ".connecting"
case .disconnected: return ".disconnected"
case .disconnecting: return ".disconnecting"
@unknown default: return "undefined"
}
}
}
extension TimeInterval {
public static var never: TimeInterval { get { TimeInterval(Int.max) } }
public static var fortnight: TimeInterval { get { TimeInterval(1209600) }}
public static var week: TimeInterval { get { TimeInterval(604800) }}
public static var day: TimeInterval { get { TimeInterval(86400) } }
public static var hour: TimeInterval { get { TimeInterval(3600) } }
public static var minute: TimeInterval { get { TimeInterval(60) } }
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Data/BatteryLog.swift
================================================
//
// BatteryLog.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import NotificationCenter
import os
// 电池日志,用于随时间监视电池电量
public class BatteryLog {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "BatteryLog")
private let dateFormatter = DateFormatter()
private let updateInterval = TimeInterval(30)
public init() {
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
// 是否启用了电池监视
UIDevice.current.isBatteryMonitoringEnabled = true
NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil)
// 设置定时时间,每隔updataInterval时间间隔,执行一次update()功能
let _ = Timer.scheduledTimer(timeInterval: updateInterval, target: self, selector: #selector(update), userInfo: nil, repeats: true)
}
private func timestamp() -> String {
let timestamp = dateFormatter.string(from: Date())
return timestamp
}
@objc func update() {
// 记录电池日志
let powerSource = (UIDevice.current.batteryState == .unplugged ? "battery" : "external")
let batteryLevel = Float(UIDevice.current.batteryLevel * 100).description
logger.debug("update (powerSource=\(powerSource),batteryLevel=\(batteryLevel))");
}
@objc func batteryLevelDidChange(_ sender: NotificationCenter) {
update()
}
@objc func batteryStateDidChange(_ sender: NotificationCenter) {
update()
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Data/SensorLogger.swift
================================================
//
// SensorLogger.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import UIKit
import os
public protocol SensorLogger {
init(subsystem: String, category: String)
func log(_ level: SensorLoggerLevel, _ message: String)
func debug(_ message: String)
func info(_ message: String)
func fault(_ message: String)
}
public enum SensorLoggerLevel: String {
case off, debug, info, fault
}
public class SpecificSensorLogger: NSObject, SensorLogger {
private let subsystem: String
private let category: String
private let dateFormatter = DateFormatter()
public required init(subsystem: String, category: String) {
self.subsystem = subsystem
self.category = category
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
}
private func suppress(_ level: SensorLoggerLevel) -> Bool {
if (BLESensorConfiguration.logLevel == .off) {
return true
}
switch level {
case .debug:
return (BLESensorConfiguration.logLevel == .info || BLESensorConfiguration.logLevel == .fault)
case .info:
return (BLESensorConfiguration.logLevel == .fault)
default:
return false
}
}
public func log(_ level: SensorLoggerLevel, _ message: String) {
guard !suppress(level) else {
return
}
let timestamp = dateFormatter.string(from: Date())
let csvMessage = message.replacingOccurrences(of: "\"", with: "'")
let quotedMessage = (message.contains(",") ? "\"" + csvMessage + "\"" : csvMessage)
let entry = timestamp + "|" + level.rawValue + "|" + subsystem + "|" + category + "|" + quotedMessage
print(entry)
}
public func debug(_ message: String) {
log(.debug, message)
}
public func info(_ message: String) {
log(.info, message)
}
public func fault(_ message: String) {
log(.fault, message)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Device.swift
================================================
//
// Device.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
public class Device : NSObject {
/// 设备注册时间戳
var createdAt: Date
/// 最后一次属性更新时间
var lastUpdatedAt: Date
/// 临时设备标识符
public var identifier: TargetIdentifier
init(_ identifier: TargetIdentifier) {
self.createdAt = Date()
self.identifier = identifier
lastUpdatedAt = createdAt
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Extensions/DataExtensions.swift
================================================
//
// DataExtensions.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import Accelerate
public extension Data {
//将Data型数据转换为十六进制编码格式的字符串
var hexEncodedString: String { get {
return map { String(format: "%02hhX", $0) }.joined()
}}
init?(hexEncodedString: String) {
guard hexEncodedString.count.isMultiple(of: 2) else {
return nil
}
if hexEncodedString.count == 0 {
self.init()
} else {
let chars = hexEncodedString.map { $0 }
let bytes = stride(from: 0, to: chars.count, by: 2)
.map { String(chars[$0]) + String(chars[$0 + 1]) }
.compactMap { UInt8($0, radix: 16) }
guard hexEncodedString.count / bytes.count == 2 else {
return nil
}
self.init(bytes)
}
}
// MARK:-将内置类型转换为Data类型
//使用mutating关键词修饰后可以修改值引用如结构体自身的属性
//使用append方法将不同位数与不同类型的数据(每8位作为一个字节)存入到Data实例中
mutating func append(_ value: UInt8) {
append(Data([value.bigEndian])) //此处append方法为向Data实例中添加字节的具体实现方法
}
mutating func append(_ value: UInt16) {
//此后对于UInt数据处理的value方法append中的append函数均为第一个append函数的调用(即代码40行)
append(UInt8(value & 0xFF).bigEndian) // LSB,最低字节
append(UInt8((value >> 8) & 0xFF).bigEndian) // MSB,最高字节
}
mutating func append(_ value: UInt32) {
append(UInt8(value & 0xFF).bigEndian) // LSB
append(UInt8((value >> 8) & 0xFF).bigEndian)
append(UInt8((value >> 16) & 0xFF).bigEndian)
append(UInt8((value >> 24) & 0xFF).bigEndian) // MSB
}
mutating func append(_ value: UInt64) {
append(UInt8(value & 0xFF).bigEndian) // LSB
append(UInt8((value >> 8) & 0xFF).bigEndian)
append(UInt8((value >> 16) & 0xFF).bigEndian)
append(UInt8((value >> 24) & 0xFF).bigEndian)
append(UInt8((value >> 32) & 0xFF).bigEndian)
append(UInt8((value >> 40) & 0xFF).bigEndian)
append(UInt8((value >> 48) & 0xFF).bigEndian)
append(UInt8((value >> 56) & 0xFF).bigEndian) // MSB
}
//以下方法与上方法作用类似:对于有符号型value,append方法将value按每8位一个字节(从低位到高位)添加到Data实例中
mutating func append(_ value: Int8) {
var int8 = value
append(Data(bytes: &int8, count: MemoryLayout.size))
//MemoryLayout(unsafe)是一个数据结构,用于保存类的内存配置。类对象的size、stride、alignment都是8个字节,这是因为struct类型是值类型数据,class是对象类型数据,使用MemoryLayout对class类型计算其内存结果实际上是对其class类型的引用指针进行操作!
}
mutating func append(_ value: Int16) {
var int16 = value
append(Data(bytes: &int16, count: MemoryLayout.size))
}
mutating func append(_ value: Int32) {
var int32 = value
append(Data(bytes: &int32, count: MemoryLayout.size))
}
mutating func append(_ value: Int64) {
var int64 = value
append(Data(bytes: &int64, count: MemoryLayout.size))
}
//MARK:-将Data类型还原为内置类型
//从字节组中提取Int8类型(LSB)
func int8(_ index: Int) -> Int8? {
guard let value = uint8(index) else {
return nil
}
return Int8(bitPattern: value)
}
//从字节组中提取UInt8类型(LSB)
func uint8(_ index: Int) -> UInt8? {
let bytes = [UInt8](self)
guard index < bytes.count else {
return nil
}
return bytes[index]
}
//从字节组中提取Int16类型(LSB)
func int16(_ index: Int) -> Int16? {
guard let value = uint16(index) else {
return nil
}
return Int16(bitPattern: value)
}
//从字节组中提取UInt16类型(LSB)
func uint16(_ index: Int) -> UInt16? {
let bytes = [UInt8](self)
guard index < (bytes.count - 1) else {
return nil
}
return UInt16(bytes[index]) |
UInt16(bytes[index + 1]) << 8
}
//从字节组中提取Int32类型(LSB)
func int32(_ index: Int) -> Int32? {
guard let value = uint32(index) else {
return nil
}
return Int32(bitPattern: value)
}
//从字节组中提取UInt32类型(LSB)
func uint32(_ index: Int) -> UInt32? {
let bytes = [UInt8](self)
guard index < (bytes.count - 3) else {
return nil
}
return UInt32(bytes[index]) |
UInt32(bytes[index + 1]) << 8 |
UInt32(bytes[index + 2]) << 16 |
UInt32(bytes[index + 3]) << 24
}
//从字节组中提取Int64类型(LSB)
func int64(_ index: Int) -> Int64? {
guard let value = uint64(index) else {
return nil
}
return Int64(bitPattern: value)
}
//从字节组中提取UInt64类型(LSB)
func uint64(_ index: Int) -> UInt64? {
let bytes = [UInt8](self)
guard index < (bytes.count - 7) else {
return nil
}
return UInt64(bytes[index]) |
UInt64(bytes[index + 1]) << 8 |
UInt64(bytes[index + 2]) << 16 |
UInt64(bytes[index + 3]) << 24 |
UInt64(bytes[index + 4]) << 32 |
UInt64(bytes[index + 5]) << 40 |
UInt64(bytes[index + 6]) << 48 |
UInt64(bytes[index + 7]) << 56
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Extensions/DateExtensions.swift
================================================
//
// DateExtensions.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
extension Date {
//定义Date类型运算符“-”
static func - (lhs: Date, rhs: Date) -> TimeInterval {
return lhs.timeIntervalSince1970 - rhs.timeIntervalSince1970
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Location/LocationSensor.swift
================================================
//
// LocationSensor.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
import CoreLocation
protocol TravelSensor : Sensor {
}
/**
可选配置:基于定位的位置传感器,在全局配置中将mobilitySensorEnabled设置为nil不使用GPS位置。
Pioneer使用位置传感器仅用于评估行程范围,而非记录用户的位置信息。用户的行程范围未来将有可能作为评估用户近期社交活跃度的依据。
使用该功能需要定位权限
Requires : Signing & Capabilities : BackgroundModes : LocationUpdates = YES
同时使用该功能需要隐私权限请求
Requires : Info.plist : Privacy - Location When In Use Usage Description
Requires : Info.plist : Privacy - Location Always and When In Use Usage Description
*/
class SpecificTravelSensor : NSObject, TravelSensor, CLLocationManagerDelegate {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "SpecificTravelSensor")
private var delegates: [SensorDelegate] = []
private let locationManager = CLLocationManager()
private let rangeForBeacon: UUID?
// 根据CoreLocation的定义,移动传感器分辨率最小为3km
public static let minimumResolution: Distance = Distance(kCLLocationAccuracyThreeKilometers)
// 最后一个位置仅用于计算累计行驶距离
private let resolution: Distance
private var lastLocation: CLLocation?
private var lastUpdate: Date?
private var totalDistance: Distance = 0
init(resolution: Distance = minimumResolution, rangeForBeacon: UUID? = nil) {
self.resolution = resolution
self.rangeForBeacon = rangeForBeacon
super.init()
let accuracy = SpecificTravelSensor.locationAccuracy(resolution)
logger.debug("init(resolution=\(resolution),accuracy=\(accuracy),rangeForBeacon=\(rangeForBeacon == nil ? "disabled" : rangeForBeacon!.description))")
locationManager.delegate = self
//申请位置权限并初始化位置管理器
if #available(iOS 13.4, *) {
self.locationManager.requestWhenInUseAuthorization()
} else {
self.locationManager.requestAlwaysAuthorization()
}
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.desiredAccuracy = accuracy
locationManager.distanceFilter = resolution
locationManager.allowsBackgroundLocationUpdates = true
locationManager.showsBackgroundLocationIndicator = false
}
// 根据所需的分辨率确定所需的定位精度
private static func locationAccuracy(_ resolution: Distance) -> CLLocationAccuracy {
if resolution < 10 {
return kCLLocationAccuracyBest
}
if resolution < 100 {
return kCLLocationAccuracyNearestTenMeters
}
if resolution < 1000 {
return kCLLocationAccuracyHundredMeters
}
if resolution < 3000 {
return kCLLocationAccuracyKilometer
}
return kCLLocationAccuracyThreeKilometers
}
func add(delegate: SensorDelegate) {
delegates.append(delegate)
}
func start() {
logger.debug("start")
locationManager.startUpdatingLocation()
logger.debug("startUpdatingLocation")
guard let beaconUUID = rangeForBeacon else {
return
}
if #available(iOS 13.0, *) {
locationManager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
logger.debug("startRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
} else {
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
locationManager.startRangingBeacons(in: beaconRegion)
logger.debug("startRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.uuidString)))")
}
}
func stop() {
logger.debug("stop")
locationManager.stopUpdatingLocation()
logger.debug("stopUpdatingLocation")
guard let beaconUUID = rangeForBeacon else {
return
}
if #available(iOS 13.0, *) {
locationManager.stopRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
logger.debug("stopRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
} else {
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
locationManager.stopRangingBeacons(in: beaconRegion)
logger.debug("stopRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.description))")
}
}
// MARK:- CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
var state = SensorState.off
if status == CLAuthorizationStatus.authorizedWhenInUse {
self.locationManager.requestAlwaysAuthorization()
state = .on
}
if status == CLAuthorizationStatus.authorizedAlways {
state = .on
}
if status == CLAuthorizationStatus.notDetermined {
if #available(iOS 13.4, *) {
self.locationManager.requestWhenInUseAuthorization()
} else {
self.locationManager.requestAlwaysAuthorization()
}
locationManager.stopUpdatingLocation()
locationManager.startUpdatingLocation()
}
if status != CLAuthorizationStatus.notDetermined {
delegates.forEach({ $0.sensor(.TRAVEL, didUpdateState: state) })
}
}
@available(iOS 14.0, *)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
var state = SensorState.off
if manager.authorizationStatus == CLAuthorizationStatus.authorizedWhenInUse {
self.locationManager.requestAlwaysAuthorization()
state = .on
}
if manager.authorizationStatus == CLAuthorizationStatus.authorizedAlways {
state = .on
}
if manager.authorizationStatus == CLAuthorizationStatus.notDetermined {
locationManager.requestWhenInUseAuthorization()
locationManager.stopUpdatingLocation()
locationManager.startUpdatingLocation()
}
if manager.authorizationStatus != CLAuthorizationStatus.notDetermined {
delegates.forEach({ $0.sensor(.TRAVEL, didUpdateState: state) })
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 仅在启用了行程传感器的情况下处理位置数据
guard let resolution = BLESensorConfiguration.TravelSensorEnabled else {
return
}
guard locations.count > 0 else {
return
}
// 累计总的行驶距离并以设置的分辨率报告,注意:在移动性检测中不报告实际行驶位置和方向,只报告以分辨率单位表示的累积行驶距离。
locations.forEach() { location in
guard let lastLocation = lastLocation, let lastUpdate = lastUpdate else {
self.lastLocation = location
self.lastUpdate = location.timestamp
return
}
// 计算总的行程距离。注意,行程距离按直线计算(尽管这看来时粗略的。
let distance = location.distance(from: lastLocation)
totalDistance += distance
logger.debug("didUpdateLocations(distance=\(distance))")
// 行程数据仅以设置的分辨率报告
if totalDistance >= resolution {
let didVisit = Location(value: TravelLocationReference(distance: totalDistance), time: (start: lastUpdate, end: location.timestamp))
delegates.forEach { $0.sensor(.TRAVEL, didVisit: didVisit) }
totalDistance = 0
self.lastUpdate = location.timestamp
}
self.lastLocation = location
}
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Payload/PayloadDataMatcher.swift
================================================
//
// PayloadDataMatcher.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
protocol PayloadDataMatcher {
func matches(_ timestamp: PayloadTimestamp, _ matchingKeysList: [MatchingKey], _ queue: DispatchQueue) -> Bool
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Payload/PayloadDataSupplier.swift
================================================
//
// SpecificDataSupplier.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
public protocol PayloadDataSupplier {
func payload(_ timestamp: PayloadTimestamp, device: Device?) -> PayloadData?
func payload(_ data: Data) -> [PayloadData]
}
public extension PayloadDataSupplier {
func payload(_ data: Data) -> [PayloadData] {
let fixedLengthPayload = payload(PayloadTimestamp(), device: nil)
var payloads: [PayloadData] = []
if let fixedLengthPayload = fixedLengthPayload {
let payloadLength = fixedLengthPayload.count
var indexStart = 0, indexEnd = payloadLength
while indexEnd <= data.count {
let payload = PayloadData(data.subdata(in: indexStart.. 0 else {
return ""
}
guard data.count > 3 else {
return data.base64EncodedString()
}
return String(data.subdata(in: 3.. Data?{
return data.subdata(in: 0..<5)
}
public func getContactIdentifier() -> String?{
return data.subdata(in: 5..<21).base64EncodedString()
}
public func getStartTime() -> Date?{
let timeInterval = TimeInterval(data.uint64(21)! / 1000)
return Date(timeIntervalSince1970: timeInterval)
}
public func getEndTime(_ startTime:Date) -> Date?{
let timeInterval = startTime.timeIntervalSince1970 + TimeInterval.minute * 6
return Date(timeIntervalSince1970: timeInterval)
}
public func getMAC() -> String?{
return data.subdata(in: 29.. Bool{
guard let startTime = self.getStartTime(),let endTime = self.getEndTime(startTime) else{
return false
}
guard startTime <= timestamp,endTime >= timestamp else{
return false
}
return true
}
// MARK:- Data
public var count: Int { get { data.count }}
public var hexEncodedString: String { get { data.hexEncodedString }}
public func base64EncodedString() -> String {
return data.base64EncodedString()
}
public func subdata(in range: Range) -> Data {
return data.subdata(in: range)
}
// MARK:- Hashable
public var hashValue: Int { get { data.hashValue } }
public func hash(into hasher: inout Hasher) {
data.hash(into: &hasher)
}
// MARK:- Equatable
public static func ==(lhs: PayloadData, rhs: PayloadData) -> Bool {
return lhs.data == rhs.data
}
// MARK:- Append
public func append(_ other: PayloadData) {
data.append(other.data)
}
public func append(_ other: Data) {
data.append(other)
}
public func append(_ other: Int8) {
data.append(other)
}
public func append(_ other: Int16) {
data.append(other)
}
public func append(_ other: Int32) {
data.append(other)
}
public func append(_ other: Int64) {
data.append(other)
}
public func append(_ other: UInt8) {
data.append(other)
}
public func append(_ other: UInt16) {
data.append(other)
}
public func append(_ other: UInt32) {
data.append(other)
}
public func append(_ other: UInt64) {
data.append(other)
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Payload/SpecificImplement/SpecificPayloadDataMatcher.swift
================================================
//
// SpecificPayloadDataMatcher.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
protocol SimplePayloadDataMatcher : PayloadDataMatcher {
}
public class SpecificPayloadDataMatcher : SimplePayloadDataMatcher {
private var delegate: ContactLogManagerDelegate
public init(delegate: ContactLogManagerDelegate){
self.delegate = delegate
}
public func createPredicate(timestamp:Date = Date(),range: Int = 14,contactIdentifier: String) -> NSPredicate{
let (second,_) = Int(timestamp.timeIntervalSince1970).remainderReportingOverflow(dividingBy: 86400)
let middle = timestamp.timeIntervalSince1970 - TimeInterval(second) - Double(range) * TimeInterval.fortnight
let timeSinceRange = Date.init(timeIntervalSince1970: middle)
return NSPredicate.init(format: "startTime >= %@ && startTime <= %@ && contactIdentifier == %@ ",timeSinceRange as CVarArg,timestamp as CVarArg,contactIdentifier)
}
// MARK:- SimplePayloadDataMatcher
public func matches(_ timestamp: PayloadTimestamp, _ matchingKeysList: [MatchingKey], _ queue: DispatchQueue) -> Bool {
var matcheResult: Bool = false
let semaphore = DispatchSemaphore.init(value: 0)
queue.async {
matchingKeysList.forEach{ matchingKey in
KeyManager.contactKeys(matchingKey).forEach{ contactKey in
let contactIdentifier = KeyManager.contactIdentifier(contactKey).base64EncodedString()
if self.isVaild(self.delegate.matching(predicate: self.createPredicate(timestamp: timestamp,contactIdentifier: contactIdentifier)),matchingKey){
matcheResult = true
semaphore.signal()
}
}
}
semaphore.signal()
}
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
return matcheResult
}
public func isVaild(_ encounterList: [InsideEncounter], _ matchingKey: MatchingKey) -> Bool{
guard encounterList.filter({
var message = Data.init(base64Encoded: $0.contactIdentifier)!
message.append(UInt64($0.startTime.timeIntervalSince1970 * 1000))
return $0.mac == Crypto.MAC(message: message, key: matchingKey).base64EncodedString()
}).isEmpty else{
return true
}
return false
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/Payload/SpecificImplement/SpecificPayloadDataSupplier.swift
================================================
//
// SpecificPayloadDataSupplier.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
public protocol SimplePayloadDataSupplier : PayloadDataSupplier {
}
public class SpecificPayloadDataSupplier : SimplePayloadDataSupplier {
private let logger = SpecificSensorLogger(subsystem: "Sensor", category: "Payload.SpecificPayloadDataSupplier")
public static let payloadLength: Int = 61
private let commonHeader: Data
public let matchingKeys: [MatchingKey]
private var day: Int?
private var contactIdentifiers: [ContactIdentifier]?
public init(commonHeader: Data, secretKey: SecretKey) {
self.commonHeader = commonHeader
matchingKeys = KeyManager.matchingKeys(secretKey)
}
public func matchingKey(_ time: Date) -> MatchingKey? {
let day = KeyManager.day(time)
guard day >= 0, day < matchingKeys.count else {
logger.fault("Matching key out of day range (time=\(time),day=\(day)))")
return nil
}
return matchingKeys[day]
}
private func contactIdentifier(_ time: Date) -> ContactIdentifier? {
let day = KeyManager.day(time)
let period = KeyManager.period(time)
guard day >= 0, day < matchingKeys.count else {
logger.fault("Contact identifier out of day range (time=\(time),day=\(day)))")
return nil
}
// 按需生成和缓存特定日期的接触人密钥
if self.day != day {
contactIdentifiers = KeyManager.contactKeys(matchingKeys[day]).map({ KeyManager.contactIdentifier($0) })
self.day = day
}
guard let contactIdentifiers = contactIdentifiers else {
logger.fault("Contact identifiers unavailable (time=\(time),day=\(day)))")
return nil
}
guard period >= 0, period < contactIdentifiers.count else {
logger.fault("Contact identifier out of period range (time=\(time),period=\(period)))")
return nil
}
// 防御性检查
guard contactIdentifiers[period].count == 16 else {
logger.fault("Contact identifier not 16 bytes (time=\(time),count=\(contactIdentifiers[period].count))")
return nil
}
return contactIdentifiers[period]
}
// MARK:- SimplePayloadDataSupplier
public func payload(_ timestamp: Date = Date(), device: Device?) -> PayloadData? {
let payloadData = PayloadData()
var message: Data = Data.init()
payloadData.append(commonHeader)
if let contactIdentifier = contactIdentifier(timestamp) {
message.append(contactIdentifier)
message.append(UInt64(timestamp.timeIntervalSince1970 * 1000))
} else {
message = Data.init(repeating: 0, count: 24)
}
payloadData.append(message)
payloadData.append(Crypto.MAC(message: message, key: self.matchingKey(timestamp)!))
return payloadData
}
public func payload(_ data: Data) -> [PayloadData] {
var payloads: [PayloadData] = []
var indexStart = 0, indexEnd = SpecificPayloadDataSupplier.payloadLength
while indexEnd <= data.count {
let payload = PayloadData(data.subdata(in: indexStart.. PayloadData{
return (self.payloadDataSupplier?.payload(PayloadTimestamp(), device: nil))!
}
private func deviceModel() -> String {
var deviceInformation = utsname()
uname(&deviceInformation)
let mirror = Mirror(reflecting: deviceInformation.machine)
return mirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else {
return identifier
}
return identifier + String(UnicodeScalar(UInt8(value)))
}
}
public func add(delegate: SensorDelegate) {
sensorArray.forEach { $0.add(delegate: delegate) }
}
public func start() {
logger.debug("start")
sensorArray.forEach { $0.start() }
}
public func stop() {
logger.debug("stop")
sensorArray.forEach { $0.stop() }
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer/Sensor/SensorDelegate.swift
================================================
//
// SensorDelegate.swift
// Pioneer
//
// Created by Beh on 2021/5/22.
//
import Foundation
/// 接收传感器事件的传感器委派。
public protocol SensorDelegate {
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier)
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier)
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier)
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier)
func sensor(_ sensor: SensorType, didVisit: Location?)
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData)
func sensor(_ sensor: SensorType, didUpdateState: SensorState)
}
/// 传感器委托功能都是可选的。
public extension SensorDelegate {
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {}
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {}
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier) {}
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {}
func sensor(_ sensor: SensorType, didVisit: Location?) {}
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData) {}
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {}
}
// MARK:- SensorDelegate data
public enum SensorType : String {
/// 低功耗蓝牙 Bluetooth Low Energy (BLE)
case BLE
/// 行程传感器,使用位置传感器
case TRAVEL
/// 其他未来可扩充的传感器,以更加准确的手机数据
case OTHER
}
public enum SensorState : String {
case on
case off
case unavailable
}
/// 检测目标的临时标识符(例如智能手机、信标、地点)。这可能是一个UUID,但使用字符串作为可变标识符长度。
public typealias TargetIdentifier = String
// MARK:- Proximity data
/// 用于估计传感器和目标之间接近度的原始数据,例如用于BLE的RSSI。
public struct Proximity {
public let unit: ProximityMeasurementUnit
public let value: Double
public let calibration: Calibration?
public var description: String { get {
guard let calibration = calibration else {
return "\(unit.rawValue):\(value.description)"
}
return "\(unit.rawValue):\(value.description)[\(calibration.description)]"
}}
public init(unit: ProximityMeasurementUnit, value: Double, calibration: Calibration? = nil) {
self.unit = unit
self.value = value
self.calibration = calibration
}
}
public enum ProximityMeasurementUnit : String {
/// 接收信号强度指示
case RSSI
/// 往返时间
case RTT
}
/// 用于解释传感器和目标之间接近值的校准数据,例如BLE的发射功率。
public struct Calibration {
public let unit: CalibrationMeasurementUnit
public let value: Double
public var description: String { get {
unit.rawValue + ":" + value.description
}}
}
/// 用于校准近距离传输数据值的测量装置,例如BLE传输功率
public enum CalibrationMeasurementUnit : String {
/// 蓝牙发射功率,用于描述1米处的预期RSSI,用于解释测量的RSSI值。
case BLETransmitPower
}
// MARK:- Location data
/// 用于估算间接暴露的原始位置数据
public struct Location {
let value: LocationReference
let time: (start: Date, end: Date)
public var description: String { get {
value.description + ":[from=" + time.start.description + ",to=" + time.end.description + "]"
}}
}
public protocol LocationReference {
var description: String { get }
}
// 距离以单位米(m)估计
public typealias Distance = Double
// 沿任何方向行进的距离(以米为单位),作为移动范围的指标。
public struct TravelLocationReference : LocationReference {
let distance: Distance
public var description: String {
get {
"Travel(distance=\(distance))"
}
}
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
9D561A0026691F2700213A58 /* RandomGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5619FF26691F2700213A58 /* RandomGenerator.swift */; };
9D561A0226691F4400213A58 /* DigestRandomNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0126691F4400213A58 /* DigestRandomNumber.swift */; };
9D561A0426691F6A00213A58 /* GeneralDigest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0326691F6A00213A58 /* GeneralDigest.swift */; };
9D561A0626691FB000213A58 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0526691FB000213A58 /* Util.swift */; };
9D561A0826691FDE00213A58 /* CipherParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0726691FDE00213A58 /* CipherParameters.swift */; };
9D561A0A26691FF700213A58 /* KeyParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0926691FF700213A58 /* KeyParameter.swift */; };
9D561A0C2669201100213A58 /* Memoable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0B2669201100213A58 /* Memoable.swift */; };
9D561A0E2669202D00213A58 /* ExtendedDigest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0D2669202D00213A58 /* ExtendedDigest.swift */; };
9D561A102669204500213A58 /* PioneerHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A0F2669204500213A58 /* PioneerHash.swift */; };
9D561A122669207100213A58 /* PioneerHMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A112669207100213A58 /* PioneerHMac.swift */; };
9D561A142669208B00213A58 /* Digest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A132669208B00213A58 /* Digest.swift */; };
9D561A16266920A600213A58 /* SM3Digest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A15266920A600213A58 /* SM3Digest.swift */; };
9D561A18266920D000213A58 /* MAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A17266920D000213A58 /* MAC.swift */; };
9D561A1A266920ED00213A58 /* HMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A19266920ED00213A58 /* HMAC.swift */; };
9D561A1C26692A0500213A58 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D561A1B26692A0500213A58 /* Crypto.swift */; };
9D7F33F226592265003DE03D /* Pioneer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D7F33E826592265003DE03D /* Pioneer.framework */; };
9D7F33F726592265003DE03D /* PioneerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F33F626592265003DE03D /* PioneerTests.swift */; };
9D7F33F926592265003DE03D /* Pioneer.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D7F33EB26592265003DE03D /* Pioneer.h */; settings = {ATTRIBUTES = (Public, ); }; };
9D7F34092659238D003DE03D /* Register.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F34082659238D003DE03D /* Register.swift */; };
9D7F340B265923D1003DE03D /* ManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F340A265923D1003DE03D /* ManagerDelegate.swift */; };
9D7F340D265923F2003DE03D /* GlobalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F340C265923F2003DE03D /* GlobalConfiguration.swift */; };
9D7F340F2659240C003DE03D /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F340E2659240C003DE03D /* Connection.swift */; };
9D7F341126592457003DE03D /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F341026592457003DE03D /* KeyManager.swift */; };
9D7F3414265924F8003DE03D /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3413265924F8003DE03D /* DateExtensions.swift */; };
9D7F34162659251C003DE03D /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F34152659251C003DE03D /* DataExtensions.swift */; };
9D7F341E2659268D003DE03D /* BLESensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F341D2659268C003DE03D /* BLESensor.swift */; };
9D7F34202659269E003DE03D /* BLEDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F341F2659269E003DE03D /* BLEDatabase.swift */; };
9D7F3422265926B1003DE03D /* BLEUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3421265926B1003DE03D /* BLEUtilities.swift */; };
9D7F3424265926C1003DE03D /* BLETransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3423265926C1003DE03D /* BLETransmitter.swift */; };
9D7F3426265926E8003DE03D /* BLEReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3425265926E8003DE03D /* BLEReceiver.swift */; };
9D7F34282659279B003DE03D /* LocationSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F34272659279B003DE03D /* LocationSensor.swift */; };
9D7F342A265927F8003DE03D /* SensorLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3429265927F8003DE03D /* SensorLogger.swift */; };
9D7F342E26592863003DE03D /* BatteryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F342D26592863003DE03D /* BatteryLog.swift */; };
9D7F3430265928C5003DE03D /* PayloadDataSupplier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F342F265928C5003DE03D /* PayloadDataSupplier.swift */; };
9D7F3432265928E2003DE03D /* PayloadDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F3431265928E2003DE03D /* PayloadDataMatcher.swift */; };
9D7F343426592938003DE03D /* Sensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F343326592938003DE03D /* Sensor.swift */; };
9D7F34362659294B003DE03D /* SensorArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F34352659294B003DE03D /* SensorArray.swift */; };
9D7F343926592972003DE03D /* SensorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F343826592972003DE03D /* SensorDelegate.swift */; };
9D7F343B26592983003DE03D /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7F343A26592983003DE03D /* Device.swift */; };
9D9487CA26593EB60094EFBB /* SpecificPayloadDataSupplier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9487C926593EB60094EFBB /* SpecificPayloadDataSupplier.swift */; };
9D9487CC26593F850094EFBB /* SpecificPayloadDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9487CB26593F850094EFBB /* SpecificPayloadDataMatcher.swift */; };
9D9487D8265943260094EFBB /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9487D7265943260094EFBB /* UploadManager.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
9D7F33F326592265003DE03D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9D7F33DF26592265003DE03D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9D7F33E726592265003DE03D;
remoteInfo = Pioneer;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
9D5619FF26691F2700213A58 /* RandomGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomGenerator.swift; sourceTree = ""; };
9D561A0126691F4400213A58 /* DigestRandomNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigestRandomNumber.swift; sourceTree = ""; };
9D561A0326691F6A00213A58 /* GeneralDigest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralDigest.swift; sourceTree = ""; };
9D561A0526691FB000213A58 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; };
9D561A0726691FDE00213A58 /* CipherParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherParameters.swift; sourceTree = ""; };
9D561A0926691FF700213A58 /* KeyParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyParameter.swift; sourceTree = ""; };
9D561A0B2669201100213A58 /* Memoable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memoable.swift; sourceTree = ""; };
9D561A0D2669202D00213A58 /* ExtendedDigest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedDigest.swift; sourceTree = ""; };
9D561A0F2669204500213A58 /* PioneerHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PioneerHash.swift; sourceTree = ""; };
9D561A112669207100213A58 /* PioneerHMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PioneerHMac.swift; sourceTree = ""; };
9D561A132669208B00213A58 /* Digest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Digest.swift; sourceTree = ""; };
9D561A15266920A600213A58 /* SM3Digest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SM3Digest.swift; sourceTree = ""; };
9D561A17266920D000213A58 /* MAC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MAC.swift; sourceTree = ""; };
9D561A19266920ED00213A58 /* HMAC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HMAC.swift; sourceTree = ""; };
9D561A1B26692A0500213A58 /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; };
9D7F33E826592265003DE03D /* Pioneer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pioneer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9D7F33EB26592265003DE03D /* Pioneer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pioneer.h; sourceTree = ""; };
9D7F33EC26592265003DE03D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9D7F33F126592265003DE03D /* PioneerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PioneerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
9D7F33F626592265003DE03D /* PioneerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PioneerTests.swift; sourceTree = ""; };
9D7F33F826592265003DE03D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9D7F34082659238D003DE03D /* Register.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Register.swift; sourceTree = ""; };
9D7F340A265923D1003DE03D /* ManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagerDelegate.swift; sourceTree = ""; };
9D7F340C265923F2003DE03D /* GlobalConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalConfiguration.swift; sourceTree = ""; };
9D7F340E2659240C003DE03D /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; };
9D7F341026592457003DE03D /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = ""; };
9D7F3413265924F8003DE03D /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; };
9D7F34152659251C003DE03D /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = ""; };
9D7F341D2659268C003DE03D /* BLESensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESensor.swift; sourceTree = ""; };
9D7F341F2659269E003DE03D /* BLEDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEDatabase.swift; sourceTree = ""; };
9D7F3421265926B1003DE03D /* BLEUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEUtilities.swift; sourceTree = ""; };
9D7F3423265926C1003DE03D /* BLETransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLETransmitter.swift; sourceTree = ""; };
9D7F3425265926E8003DE03D /* BLEReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEReceiver.swift; sourceTree = ""; };
9D7F34272659279B003DE03D /* LocationSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSensor.swift; sourceTree = ""; };
9D7F3429265927F8003DE03D /* SensorLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorLogger.swift; sourceTree = ""; };
9D7F342D26592863003DE03D /* BatteryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLog.swift; sourceTree = ""; };
9D7F342F265928C5003DE03D /* PayloadDataSupplier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayloadDataSupplier.swift; sourceTree = ""; };
9D7F3431265928E2003DE03D /* PayloadDataMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayloadDataMatcher.swift; sourceTree = ""; };
9D7F343326592938003DE03D /* Sensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sensor.swift; sourceTree = ""; };
9D7F34352659294B003DE03D /* SensorArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorArray.swift; sourceTree = ""; };
9D7F343826592972003DE03D /* SensorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorDelegate.swift; sourceTree = ""; };
9D7F343A26592983003DE03D /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; };
9D9487C926593EB60094EFBB /* SpecificPayloadDataSupplier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecificPayloadDataSupplier.swift; sourceTree = ""; };
9D9487CB26593F850094EFBB /* SpecificPayloadDataMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecificPayloadDataMatcher.swift; sourceTree = ""; };
9D9487D7265943260094EFBB /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
9D7F33E526592265003DE03D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
9D7F33EE26592265003DE03D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9D7F33F226592265003DE03D /* Pioneer.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9D5619FE26691EBE00213A58 /* Crypto */ = {
isa = PBXGroup;
children = (
9D5619FF26691F2700213A58 /* RandomGenerator.swift */,
9D561A1B26692A0500213A58 /* Crypto.swift */,
9D561A0126691F4400213A58 /* DigestRandomNumber.swift */,
9D561A0326691F6A00213A58 /* GeneralDigest.swift */,
9D561A0526691FB000213A58 /* Util.swift */,
9D561A0726691FDE00213A58 /* CipherParameters.swift */,
9D561A0926691FF700213A58 /* KeyParameter.swift */,
9D561A0F2669204500213A58 /* PioneerHash.swift */,
9D561A112669207100213A58 /* PioneerHMac.swift */,
9D561A132669208B00213A58 /* Digest.swift */,
9D561A15266920A600213A58 /* SM3Digest.swift */,
9D561A17266920D000213A58 /* MAC.swift */,
9D561A19266920ED00213A58 /* HMAC.swift */,
9D561A0B2669201100213A58 /* Memoable.swift */,
9D561A0D2669202D00213A58 /* ExtendedDigest.swift */,
);
path = Crypto;
sourceTree = "";
};
9D7F33DE26592265003DE03D = {
isa = PBXGroup;
children = (
9D7F33EA26592265003DE03D /* Pioneer */,
9D7F33F526592265003DE03D /* PioneerTests */,
9D7F33E926592265003DE03D /* Products */,
);
sourceTree = "";
};
9D7F33E926592265003DE03D /* Products */ = {
isa = PBXGroup;
children = (
9D7F33E826592265003DE03D /* Pioneer.framework */,
9D7F33F126592265003DE03D /* PioneerTests.xctest */,
);
name = Products;
sourceTree = "";
};
9D7F33EA26592265003DE03D /* Pioneer */ = {
isa = PBXGroup;
children = (
9D5619FE26691EBE00213A58 /* Crypto */,
9D7F340326592339003DE03D /* Manager */,
9D7F34072659236A003DE03D /* Sensor */,
9D7F33EB26592265003DE03D /* Pioneer.h */,
9D7F33EC26592265003DE03D /* Info.plist */,
);
path = Pioneer;
sourceTree = "";
};
9D7F33F526592265003DE03D /* PioneerTests */ = {
isa = PBXGroup;
children = (
9D7F33F626592265003DE03D /* PioneerTests.swift */,
9D7F33F826592265003DE03D /* Info.plist */,
);
path = PioneerTests;
sourceTree = "";
};
9D7F340326592339003DE03D /* Manager */ = {
isa = PBXGroup;
children = (
9D7F34082659238D003DE03D /* Register.swift */,
9D7F340A265923D1003DE03D /* ManagerDelegate.swift */,
9D9487D7265943260094EFBB /* UploadManager.swift */,
9D7F340C265923F2003DE03D /* GlobalConfiguration.swift */,
9D7F340E2659240C003DE03D /* Connection.swift */,
9D7F341026592457003DE03D /* KeyManager.swift */,
);
path = Manager;
sourceTree = "";
};
9D7F34072659236A003DE03D /* Sensor */ = {
isa = PBXGroup;
children = (
9D7F341B26592574003DE03D /* Payload */,
9D7F341A26592566003DE03D /* Data */,
9D7F341826592553003DE03D /* Location */,
9D7F34172659254B003DE03D /* BLE */,
9D7F3412265924D6003DE03D /* Extensions */,
9D7F343326592938003DE03D /* Sensor.swift */,
9D7F34352659294B003DE03D /* SensorArray.swift */,
9D7F343826592972003DE03D /* SensorDelegate.swift */,
9D7F343A26592983003DE03D /* Device.swift */,
);
path = Sensor;
sourceTree = "";
};
9D7F3412265924D6003DE03D /* Extensions */ = {
isa = PBXGroup;
children = (
9D7F3413265924F8003DE03D /* DateExtensions.swift */,
9D7F34152659251C003DE03D /* DataExtensions.swift */,
);
path = Extensions;
sourceTree = "";
};
9D7F34172659254B003DE03D /* BLE */ = {
isa = PBXGroup;
children = (
9D7F341D2659268C003DE03D /* BLESensor.swift */,
9D7F341F2659269E003DE03D /* BLEDatabase.swift */,
9D7F3421265926B1003DE03D /* BLEUtilities.swift */,
9D7F3423265926C1003DE03D /* BLETransmitter.swift */,
9D7F3425265926E8003DE03D /* BLEReceiver.swift */,
);
path = BLE;
sourceTree = "";
};
9D7F341826592553003DE03D /* Location */ = {
isa = PBXGroup;
children = (
9D7F34272659279B003DE03D /* LocationSensor.swift */,
);
path = Location;
sourceTree = "";
};
9D7F341A26592566003DE03D /* Data */ = {
isa = PBXGroup;
children = (
9D7F3429265927F8003DE03D /* SensorLogger.swift */,
9D7F342D26592863003DE03D /* BatteryLog.swift */,
);
path = Data;
sourceTree = "";
};
9D7F341B26592574003DE03D /* Payload */ = {
isa = PBXGroup;
children = (
9D7F342F265928C5003DE03D /* PayloadDataSupplier.swift */,
9D7F3431265928E2003DE03D /* PayloadDataMatcher.swift */,
9D7F341C265925A5003DE03D /* SpecificImplement */,
);
path = Payload;
sourceTree = "";
};
9D7F341C265925A5003DE03D /* SpecificImplement */ = {
isa = PBXGroup;
children = (
9D9487C926593EB60094EFBB /* SpecificPayloadDataSupplier.swift */,
9D9487CB26593F850094EFBB /* SpecificPayloadDataMatcher.swift */,
);
path = SpecificImplement;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
9D7F33E326592265003DE03D /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
9D7F33F926592265003DE03D /* Pioneer.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
9D7F33E726592265003DE03D /* Pioneer */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9D7F33FC26592265003DE03D /* Build configuration list for PBXNativeTarget "Pioneer" */;
buildPhases = (
9D7F33E326592265003DE03D /* Headers */,
9D7F33E426592265003DE03D /* Sources */,
9D7F33E526592265003DE03D /* Frameworks */,
9D7F33E626592265003DE03D /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Pioneer;
productName = Pioneer;
productReference = 9D7F33E826592265003DE03D /* Pioneer.framework */;
productType = "com.apple.product-type.framework";
};
9D7F33F026592265003DE03D /* PioneerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9D7F33FF26592265003DE03D /* Build configuration list for PBXNativeTarget "PioneerTests" */;
buildPhases = (
9D7F33ED26592265003DE03D /* Sources */,
9D7F33EE26592265003DE03D /* Frameworks */,
9D7F33EF26592265003DE03D /* Resources */,
);
buildRules = (
);
dependencies = (
9D7F33F426592265003DE03D /* PBXTargetDependency */,
);
name = PioneerTests;
productName = PioneerTests;
productReference = 9D7F33F126592265003DE03D /* PioneerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
9D7F33DF26592265003DE03D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1250;
TargetAttributes = {
9D7F33E726592265003DE03D = {
CreatedOnToolsVersion = 12.5;
LastSwiftMigration = 1250;
};
9D7F33F026592265003DE03D = {
CreatedOnToolsVersion = 12.5;
};
};
};
buildConfigurationList = 9D7F33E226592265003DE03D /* Build configuration list for PBXProject "Pioneer" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 9D7F33DE26592265003DE03D;
productRefGroup = 9D7F33E926592265003DE03D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
9D7F33E726592265003DE03D /* Pioneer */,
9D7F33F026592265003DE03D /* PioneerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
9D7F33E626592265003DE03D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
9D7F33EF26592265003DE03D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
9D7F33E426592265003DE03D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D561A0E2669202D00213A58 /* ExtendedDigest.swift in Sources */,
9D7F340B265923D1003DE03D /* ManagerDelegate.swift in Sources */,
9D7F343426592938003DE03D /* Sensor.swift in Sources */,
9D7F341126592457003DE03D /* KeyManager.swift in Sources */,
9D7F340D265923F2003DE03D /* GlobalConfiguration.swift in Sources */,
9D7F34092659238D003DE03D /* Register.swift in Sources */,
9D7F3432265928E2003DE03D /* PayloadDataMatcher.swift in Sources */,
9D9487CC26593F850094EFBB /* SpecificPayloadDataMatcher.swift in Sources */,
9D561A0A26691FF700213A58 /* KeyParameter.swift in Sources */,
9D561A1C26692A0500213A58 /* Crypto.swift in Sources */,
9D9487CA26593EB60094EFBB /* SpecificPayloadDataSupplier.swift in Sources */,
9D9487D8265943260094EFBB /* UploadManager.swift in Sources */,
9D561A142669208B00213A58 /* Digest.swift in Sources */,
9D7F3426265926E8003DE03D /* BLEReceiver.swift in Sources */,
9D7F34282659279B003DE03D /* LocationSensor.swift in Sources */,
9D561A0626691FB000213A58 /* Util.swift in Sources */,
9D7F34162659251C003DE03D /* DataExtensions.swift in Sources */,
9D561A1A266920ED00213A58 /* HMAC.swift in Sources */,
9D7F342E26592863003DE03D /* BatteryLog.swift in Sources */,
9D7F343B26592983003DE03D /* Device.swift in Sources */,
9D7F34202659269E003DE03D /* BLEDatabase.swift in Sources */,
9D561A18266920D000213A58 /* MAC.swift in Sources */,
9D7F343926592972003DE03D /* SensorDelegate.swift in Sources */,
9D561A122669207100213A58 /* PioneerHMac.swift in Sources */,
9D7F3422265926B1003DE03D /* BLEUtilities.swift in Sources */,
9D561A0426691F6A00213A58 /* GeneralDigest.swift in Sources */,
9D561A0226691F4400213A58 /* DigestRandomNumber.swift in Sources */,
9D7F34362659294B003DE03D /* SensorArray.swift in Sources */,
9D561A16266920A600213A58 /* SM3Digest.swift in Sources */,
9D7F342A265927F8003DE03D /* SensorLogger.swift in Sources */,
9D561A0C2669201100213A58 /* Memoable.swift in Sources */,
9D7F340F2659240C003DE03D /* Connection.swift in Sources */,
9D7F3430265928C5003DE03D /* PayloadDataSupplier.swift in Sources */,
9D7F3424265926C1003DE03D /* BLETransmitter.swift in Sources */,
9D561A102669204500213A58 /* PioneerHash.swift in Sources */,
9D561A0026691F2700213A58 /* RandomGenerator.swift in Sources */,
9D7F3414265924F8003DE03D /* DateExtensions.swift in Sources */,
9D7F341E2659268D003DE03D /* BLESensor.swift in Sources */,
9D561A0826691FDE00213A58 /* CipherParameters.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9D7F33ED26592265003DE03D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D7F33F726592265003DE03D /* PioneerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
9D7F33F426592265003DE03D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9D7F33E726592265003DE03D /* Pioneer */;
targetProxy = 9D7F33F326592265003DE03D /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
9D7F33FA26592265003DE03D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
9D7F33FB26592265003DE03D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
9D7F33FD26592265003DE03D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 38MCX35GX2;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Pioneer/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.404NF.Pioneer;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
9D7F33FE26592265003DE03D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 38MCX35GX2;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Pioneer/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.404NF.Pioneer;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
9D7F340026592265003DE03D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 38MCX35GX2;
INFOPLIST_FILE = PioneerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.404NF.PioneerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
9D7F340126592265003DE03D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 38MCX35GX2;
INFOPLIST_FILE = PioneerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.404NF.PioneerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
9D7F33E226592265003DE03D /* Build configuration list for PBXProject "Pioneer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D7F33FA26592265003DE03D /* Debug */,
9D7F33FB26592265003DE03D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9D7F33FC26592265003DE03D /* Build configuration list for PBXNativeTarget "Pioneer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D7F33FD26592265003DE03D /* Debug */,
9D7F33FE26592265003DE03D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9D7F33FF26592265003DE03D /* Build configuration list for PBXNativeTarget "PioneerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D7F340026592265003DE03D /* Debug */,
9D7F340126592265003DE03D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 9D7F33DF26592265003DE03D /* Project object */;
}
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/Pioneer.xcodeproj/xcuserdata/Beh.xcuserdatad/xcschemes/xcschememanagement.plist
================================================
SchemeUserState
Pioneer.xcscheme_^#shared#^_
orderHint
1
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/PioneerTests/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
1
================================================
FILE: IOS_app/Pioneer_ios/Pioneer Framework/PioneerTests/PioneerTests.swift
================================================
//
// PioneerTests.swift
// PioneerTests
//
// Created by Beh on 2021/5/22.
//
import XCTest
@testable import Pioneer
class PioneerTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
================================================
FILE: README.md
================================================
【开源软件发布】基于蓝牙的低费用病毒追踪系统
# Pioneer 病毒追踪系统
Pioneer是一款基于蓝牙的低费用病毒追踪系统,旨在通过低成本、高效率的方式追踪病毒感染者的密切接触者,以防控病毒传播。
## 内容涵盖:
1. BLE低功耗蓝牙
2. SM3加密认证
3. Java 数据库服务器开发
4. Android 开发
5. IOS开发
6. [Test](https://github.com/lunan0320/Pioneer/tree/main/Test)
## 目录
- [1. Workflow工作流程](#1-工作流程)
- [2. Pioneer for Android 指南](#2-pioneer-for-android-指南)
- [简介](#简介)
- [硬件要求](#硬件要求)
- [部署指南](#部署指南)
- [使用指南](#使用指南)
- [1.注册](#1注册)
- [2.发现](#2发现)
- [3.感染者信息报告](#3感染者信息报告)
- [4.用户界面](#4用户界面)
- [5.安全报告](#5安全报告)
- [3. Pioneer for IOS 指南](#3-pioneer-for-ios-指南)
- [简介](#简介-1)
- [主要特点](#主要特点)
- [硬件要求](#硬件要求-1)
- [部署指南](#部署指南-1)
- [使用指南](#使用指南-1)
- [1.注册](#1注册-1)
- [2.发现](#2发现-1)
- [3.安全状态](#3安全状态)
- [4.用户界面](#4用户界面-1)
- [5.感染者信息报告](#5感染者信息报告)
- [6.安全报告](#6安全报告)
- [4.Pioneer Server 服务器指南](#4-pioneer-server-服务器指南)
- [使用说明](#使用说明)
## 1. 工作流程
> 流程示意:
>
> **病毒追踪系统(追踪可疑感染病毒的患者)**
>
> 1、左下角是在一定蓝牙范围之内的用户,这些用户会**交换身份标识符 ID**,标记为与这类用户接触过
>
> 2、当一个用户**被发现与其他的感染者用户接触过**,那说明他是可疑感染的人员,此时医疗机构会去确认该用户的体征情况,若确实感染,则给与该用户 token 作为凭证
>
> 3、此时感染者用户系统后台会自动将该用户的**感染信息上传到服务器**,服务器保存在数据库中
>
> 4、其他用户每天系统后台会自动下载已有的感染者信息,并与本地保存的标识符匹配,用于**判定是否曾经与这些感染者接触**
------
## 2. Pioneer for Android 指南
### 简介
自2019 年末,新冠病毒肆虐全球,寻找新冠感染者的密切接触者成为一大难题。一个地区出现感染者后,如果不及时找到密切接触者,将会造成很严重的后果。但是,寻找密切接触者有时需要耗费大量的人力财力。
本款app 旨在准确快速地**追踪病毒感染者的密切接触者**,做好病毒传播初期的防控工作。
1. App 的追踪系统基于**BLE 蓝牙**构建,采用**低功耗**的工作模式,并且可以在后台持续运行,保证准确完整地记录用户的接触信息。
2. 本款app 采用**去中心化**的方式,很好地保护了用户的隐私信息。
3. 安全性方面,本款app 特别使用了**国密算法SM3**,为国内用户提供安全可靠的**加密认证**方式。对于可能预见到的一系列攻击手段,本款app 均已提供防御措施,app 的安全性非常高。
### 硬件要求
Pioneer for Android 对设备最低的系统版本要求是`Android 5.0`,因此要安装并部署Pioneer 必须保证iPhone 的系统版本不低于Android 5.0
- 如果您使用的iPhone 设备的Android 系统版本低于5.0,不管是从达到Pioneer 的系统版本的需求还是希望您的iPhone 设备得益于Android 高版本的安全补丁而更加安全,我们都建议您更新Android 系统版本以使用Pioneer for Android。
- 对于部署的软件要求,为了工程代码的稳定性,建议使用`Android Studio`打开Pioneer for Android 项目工程并进行查看和部署。
### 部署指南
1. 安卓项目以压缩包的形式发送,将压缩包解压到一个路径名全为英文的文件下。
1. 打开Android Studio。点击`open`,找到项目文件位置,打开文件,这样就可以查看安卓app 的源码了。
1. 用`Android Studio`将项目打包成apk 文件,把`apk` 文件传送到手机上,即可下载app。
### 使用指南
#### 1.注册
1. 打开app 后,首先会进入登录界面。首次打开app,用户还没有注册,不能登录,必须要进行`注册`,点击注册按钮即可进入注册界面。
2. 在注册界面,用户可以输入手机号进行注册。如果提示“注册成功”,用户就可以用注册的手机号在登陆界面进行登录。首次登陆成功后app 会保存账号,以后关闭app 再打开就可以直接进入主界面。如果提示“`手机号已被注册`”,说明这个手机号已经被注册过了(即用户在第一次注册成功后,若对同一手机号再次点击注册,则会给出上述提示)。
3. 如果提示`SecretKey` 重复,已重新生成,请再次点击”说明用户app 自动生成的SecretKey 和其他人的重复了,这时app会自动在本地在生成一个新的SecretKey 并覆盖之前的。用户可以再次点击注册用手机号和新的SecretKey 绑定进行注册。
#### 2.发现
1. 用户登录后,app 才开始运行。App 会`自动广播`同时进行扫描,探测周围的开启蓝牙的设备,如果app 通过`UUID` 找到安装相同app 的设备,就会建立连接并交换数据。
2. 用户在“发现”界面可以看到`蓝牙扫描信息`和收到的其他用户的数据包的短名称。列表中会显示数据包的接收时间和最后一次更新时间,当数据包不再更新时,就会显示“停止更新”。点击列表中的数据包,就可以显示出数据包的有效期和`ContactIdentifier`。
#### 3.感染者信息报告
1. 在`Token`界面有一个Token按钮,点击按钮就可以进入Token 验证界面。
2. Token 验证是用来**上传感染者信息**的,如果用户被确诊为感染者,医院的医护人员会交给用户一个Token 验证码。
3. 用户输入Token 验证码进行验证并向服务器上传个人信息。用户如果随意输入字符串进行Token 验证,就会被提示“Token输入有误,请重新输入!”。
#### 4.用户界面
在“我”界面。用户可以查看自己手机的**型号**和当前这个**时段**,即当前所处的这**6 分钟内**的数据包的短名称。
#### 5.安全报告
1. App 每天早上8 点会定时**从服务器下载感染者信息**在本地进行**匹配**。当找到匹配时,会弹出提示信息“传感器检测到您与感染者有过接触,请及时到医院进行核酸检测。”
2. app 运行在前台就会在前台弹出提示,运行在后台就会在状态栏中弹出提示。
------
## 3. Pioneer for IOS 指南
### 简介
Pioneer for iOS 是低成本病毒追踪系统Pioneer的终端,其服务于iOS 系统下(主要是iPhone 设备)的联系人追踪并将用户的风险接触信息安全地报告给Pioneer 服务器,同时用户也可以随时从服务器中了解到自身的近期安全状态,以便快速做出反应终止风险的继续传播。
### 主要特点
1. 安全的隐私保障。
2. 有效的交互保障。Pioneer 主要使用低功耗蓝牙传感器,可选地辅以iOS 的iBeacon 和位置传感器,并通过多种技术方法使设备之间的检测率达到90%以上,同时也提供93%以上的连续性测量以给出较为准确的接触数据。
3. 较高的覆盖范围。Pioneer for iOS 对iPhone 系统版本的最低要求是iOS 12.0,正如iOS 框架Network 要求的那样,这可以达到94%的适用率,即可支持最早的iPhone 5S 之后的iPhone 手机。针对iOS-iOS 的部署率可以达到94% × 94% =88.36%。
4. 较低的功耗使用。经过基于iPhone XS 的电池标准的测试,Pioneer for iOS 每小时耗电量低于3%。
5. 持续的安全报告。Pioneer for iOS 会定期(至少每天)与Pioneer 服务器进行安全的信息交互,主要是从服务器中下载风险者身份信息并与用户自身数据库中的交互信息匹配,匹配完成后将以通知的方式告知用户自身的安全报告,使用户详细了解到自身的安全状态。
### 硬件要求
Pioneer for iOS 对设备最低的系统版本要求是iOS 12.0,因此要安装并部署Pioneer 必须保证iPhone 的系统版本不低于iOS 12.0。如果您使用的iPhone 设备的iOS 系统版本低于12.0,不管是从达到Pioneer 的系统版本的需求还是希望您的iPhone 设备得益于iOS 高版本的安全补丁而更加安全,我们都建议您更新iOS系统版本以使用Pioneer for iOS。
> 对于部署的软件要求,为了工程代码的稳定性,建议使用Xcode12.0 以上的版本打开Pioneer for iOS 项目工程并进行查看和部署。
### 部署指南
完成部署首先需要拿到Pioneer 的项目工程包,其中包括Pioneer Framework 和Pioneer for iOS 文件夹。
1. 使用Xcode 打开Pioneer for iOS -> Pioneer for iOS -> Pioneer for ios.xcodeproj 工程项目,并选择Trust and Open。
2. 在TARGETS 菜单中的Signing & Capabilities 中编辑Team 以使用您的开发者账号以及Bundle Identifier。
3. 选择项目目录中的“Pioneer.xcodeproj”,在Signing & Capabilities 中编辑Team以使用您的开发者账号以及Bundle Identifier。
4. 将您的iPhone 设备连接至您的Mac 电脑并信任,Product,Run Pioneer for iOS即可将Pioneer 安装在您希望的iPhone 设备上。
5. 在iPhone 设备上信任您的开发者账号即可开始使用Pioneer for iOS。
### 使用指南
#### 1.注册
Pioneer for iOS 在安装到iPhone 设备之后,首次使用必须注册。目前Pioneer系统要求用户使用手机号作为自身的保密身份标识,因此Pioneer 用户需要使用自己的手机号与服务器建立连接以进行注册。之后用户的注册状态会永久化储存在应用中,下次APP 开启后无需再次注册。
#### 2.发现
在Pioneer for iOS 的正常工作开始之前,应用会申请使用以下的用户隐私权限:
1. 蓝牙权限,包括后台使用权限
2. 位置权限,包括后台使用权限,但鉴于不同用户的对此的意见,这并没有作为Pioneer 的必须权限。可选的,是否需要与最终Pioneer 的发行版本有关
3. 通知权限。Pioneer 定期给出的用户安全报告需要定期通知给用户
Pioneer 在通过注册并获得蓝牙使用权限后即可按照标准的工作流程开始工作,并且无需一直占用前台,应用的内部技术实现保证了Pioneer 可以无限期地运行在后台而不被操作系统终止。但是请注意如果用户之后显示地关闭了该应用(例如用户在iPhone 近期运行应用菜单中上滑关闭了Pioneer 等),Pioneer for iOS将终止工作。
#### 3.安全状态
发现界面右上角的图标用于指示用户的安全状态,该图标会根据用户的接触
安全状态自动变化,当用户为风险者时就会变化为红色以提示用户。正常的安全状态下,该图标的颜色是绿色的。
#### 4.用户界面
用户可以在个人信息界面看到注册该Pioneer 设备的手机号码和虚拟的唯一身份标识信息。同时Pioneer 允许用户自行查看应用记录的永久化数据,在菜单栏目中点击本地数据库即可查看数据库记录的用户交互信息。
#### 5.感染者信息报告
当用户在医疗机构被确诊时,医疗机构将向Pioneer 服务器申请一次性感染者身份信息上传令牌。用户将在医疗机构的帮助下,在Pioneer for iOS 中的Token界面输入一次性Token 令牌,接着Pioneer 将会收集用户的近14 天身份信息作为感染者的身份信息报告给Pioneer 服务器。
#### 6.安全报告
Pioneer 会定期(至少每天)从Pioneer 服务器下载风险者身份信息并与本地的数据库交互身份信息进行匹配,匹配结束后会根据用户的接触安全状况出具安全报告,并通过系统通知推送给用户是用户了解自身的安全状况。同时用户也可以进入Pioneer 用户界面查看自身的安全报告。
此外,如果匹配时发现了用户与风险者进行过接触(本地数据库中有与风险者的交互记录),Pioneer 后台会自动将用户的近14 天身份信息作为风险者的身份信息报告给Pioneer 服务器。同时通知用户已经与风险者有过密切接触,提醒用户去医疗机构进行检查。
------
## 4. Pioneer Server 服务器指南
### 使用说明
1. 服务器已经成功部署到远端服务器上,APP 时可直接与服务器进行交互。
2. 远端系统为Linux 操作系统,配置Java JDK 15 环境变量。
3. C/S 交互方式下采用基于SSL/TLS 协议的HTTPS 加密传输协议。
4. 后端数据库采用的是MySQL Database,默认端口3306。在MySQL 中创建数据库Pioneer 作为与客户端APP 交互的数据库。
5. 数据库采用AES 加密存储,并对加密内容十六进制编码。
6. 数据库并发读取,采用MVCC 多版本并发控制。采用InnoDB 搜索引擎,解决了脏读、写阻塞、读并发等问题。
7. 线程池的方式来实现对于多用户请求的安全处理。
8. 设置线程连接超时、会话超时的模式,超时后自动与客户端断开连接。
9. 服务器每天0:00 定时更新MatchingKeys。每一天结束后,在0:00 时刻,服务器会自动对Infected_users 和Contacted_users 表中的MatchingKey 进行更新。更新的目的是为了使数据库中保存的永远是有效的MatchingKey。
运行示意图:
================================================
FILE: Server/bin/db.properties
================================================
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/Pioneer
username=root
password=Pioner+6
================================================
FILE: Server/src/com/bean/User.java
================================================
package com.bean;
import java.io.Serializable;
//ʵû
public class User implements Serializable{
private static final long serialVersionUID=-989796422860230162L;
private String ID; //
private String key; //Կ
private String name;
private String Username;//û
private String Password;//
private String token;
private String[] MatchingKeys=new String[14];
private String Matching_keys;
public User() {
super();
// TODO Auto-generated constructor stub
}
public User(String ID) {
super();
this.ID=ID;
}
public User(String ID,String key) {
this.ID=ID;
this.key=key;
}
public User(String id, String name, String key) {
this.ID = id;
this.key = key;
this.name = name;
}
public String getId() {
return ID;
}
public void setId(String ID) {
this.ID= ID;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return Username;
}
public void setName(String Username) {
this.Username = Username;
}
public String getPassword() {
return Password;
}
public void setPassword(String Password) {
this.Password= Password;
}
public String getUsername() {
return Username;
}
public void setUsername(String Username) {
this.Username = Username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token=token;
}
public String getMatchingKeys(int i) {
return MatchingKeys[i];
}
public void setMatchingKeys(int i,String str) {
this.MatchingKeys[i]=str;
}
public String getMatching_keys() {
return Matching_keys;
}
public void setMatching_keys(String Matching_keys) {
this.Matching_keys = Matching_keys;
}
@Override
public String toString() {
return "User [ID=" + ID + ", key=" + key + ", name=" + name + ", Username=" + Username + ", Password="
+ Password + "]";
}
}
================================================
FILE: Server/src/com/bean/UserMessage.java
================================================
package com.bean;
import java.io.Serializable;
import java.util.Arrays;
//ʵûϢ
public class UserMessage implements Serializable{
private static final long serialVersionUID=-5059525171312166179L;
private String type; //
private User user; //û
private String str; //ֵΪַ
private int log;
private String[] MatchingKeys=new String[14]; // 14matchingkeys
private String token;
//methods for user messages
public UserMessage() { }
public UserMessage(int log) {
this.log=log;
}
public UserMessage(String str) {
this.str=str;
}
public UserMessage(String type,String str) {
this.type=type;
this.str=str;
}
public UserMessage(String type,User user) {
this.type=type;
this.user=user;
}
public UserMessage(String type,User user,String token) {
this.type=type;
this.user=user;
this.token=token;
}
//getters and setters
public String getType() {
return type;
}
public User getUser() {
return user;
}
public String getStr() {
return str;
}
public int getLog() {
return log;
}
public String getToken() {
return token;
}
// set str, user, type, token
public void setType(String type) {
this.type = type;
}
public void setStr(String str) {
this.str=str;
}
public void setUser(User user) {
this.user = user;
}
public void setToken(String token) {
this.token = token;
}
public void setLog(int log) {
this.log = log;
}
// Matchingkeys
public void setMatchingKeys(int i,String MatchingKey) {
MatchingKeys[i] = MatchingKey;
}
public String[] getMatchingKeys() {
return MatchingKeys;
}
// Matchingkeys ƥ
@Override
public String toString() {
return "UserMessage [type=" + type + ", user=" + user + ", str=" + str + ", log=" + log + ",
MatchingKeys=" + Arrays.toString(MatchingKeys) + ", token=" + token + "]";
}
}
================================================
FILE: Server/src/com/controller/PioneerClient.java
================================================
package com.controller;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.util.Scanner;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import com.view.View;
// HTTPSClientʵ
public class PioneerClient {
static int item;
//private String host = "172.25.226.143";
//private String host = "192.168.43.19";
private String host = "95.179.230.181";
private int port = 446;
public static void main(String[] args) throws UnknownHostException, IOException{
PioneerClient client = new PioneerClient();
client.run();
}
PioneerClient(){
}
PioneerClient(String host, int port){
this.host = host;
this.port = port;
}
// Create the and initialize the SSLContext
private SSLContext createSSLContext(){
try{
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// keyStore.load(new FileInputStream("D:\\Java\\cert\\Pioneer.keystore"),"sducst".toCharArray());
// // Create key manager
// KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
// keyManagerFactory.init(keyStore, "sducst".toCharArray());
// KeyManager[] km = keyManagerFactory.getKeyManagers();
// keyStore.load(new FileInputStream("D:\\Java\\cert\\server.p12"), "010320".toCharArray());
//keyStore.load(new FileInputStream("D:\\Java\\cert\\server.p12"), "010320".toCharArray());
// Create key manager
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, "010320".toCharArray());
KeyManager[] km = keyManagerFactory.getKeyManagers();
// Create trust manager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(keyStore);
TrustManager[] tm = trustManagerFactory.getTrustManagers();
// Initialize SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(km, tm, null);
return sslContext;
} catch (Exception ex){
ex.printStackTrace();
}
return null;
}
// Start to run the server
public void run() throws IOException{
final int connectTimeout=5*1000;
//SSLContext
SSLContext sslContext = this.createSSLContext();
// Create socket factory
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
//Socket͵ĶṩͶ˿ں
//SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(this.host, this.port);
//SSLSocket socket = (SSLSocket) sslSocketFactory.getDefault().createSocket();
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket();
socket.connect(new InetSocketAddress(host,port), connectTimeout);//ӳʱ5s
System.out.println("This is SDU Pioneer Client!");
System.out.println("Successfully connected to the server");
System.out.println("SSL client started");
new ClientThread(socket).start();
}
static class ClientThread extends Thread{
private SSLSocket socket = null;
final int handshakeTimeout=3*1000;
final int sessionTimeout=7*1000;
ClientThread(SSLSocket sslSocket){
this.socket = sslSocket;
}
public void run() {
Scanner sc=null;
DataInputStream in=null;
DataOutputStream out=null;
try {
socket.setEnabledCipherSuites(socket.getSupportedCipherSuites());
socket.setSoTimeout(handshakeTimeout);
// Start handshake
socket.startHandshake();
socket.setSoTimeout(sessionTimeout);
// Get session after the connection is established
SSLSession sslSession = socket.getSession();
System.out.println("SSLSession :");
System.out.println("\tProtocol : "+sslSession.getProtocol());
System.out.println("\tCipher suite : "+sslSession.getCipherSuite());
//ʼ
sc=new Scanner(System.in);
in=new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
//ͨ˵ӿ̨ûϢֵΪ½Ķ
String input=null;
String output="";
String str=null;
//while(true) {
String type=null;
//tag=0ʾעû0+11λֻ+2048λSecret_key
//tag=1ʾϴǰ֤token
//tag=2ʾtoken
//tag=3.ʾԶϴ14Matching_key
//tag=4ʾͨtoken֤ϴ14Matching_key
//tag=5ʾط洢Infected_users14Matching_keys
//tag=6ʾÿ춨ʱĸµĹ
item = View.MenuView();
switch(item) {
//ݲ˵ѡʼ
case 0://˳
type="Exit";
break;
case 1://ֻע
type="LoginUser";//015536471788123
output="0";
break;
case 2://tokenǷȷϴ
type="CheckToken";
output="1";
break;
case 3://ҽԱԱToken
type="ApplyToken";//2doctor1doctor1
output="2";
break;
case 4://Զϴ
type="AutoUpload";
output="4";
break;
case 5://ͻظȾϢ
type="Download";
break;
case 6://¸ȾԼӴϢ
type="Update";
break;
}
//
output+=MenuView();//Ҫַ
System.out.println("͵Ϊ:"+output.length());
//ijȶ̬ķ
byte[] out_bytes=new byte[output.length()];
out_bytes=output.getBytes();
//bytes=input;
//ûϢ
out.write(out_bytes);
out.flush();
System.out.println("ѷϢ~:"+output);
//
//ȡ˻ط֤
//if(!type.equals("AutoUpload")) {
try {
str=in.readUTF();
System.out.println("ӷյ֤"+str);
if(type.equals("Download")) {
int i =0;
int index1=0;
int index2=0;
String temp="";
while(true) {
index1=str.indexOf("SDU",i);
if(index1==-1) break;
index2=str.indexOf("SDU",index1+3);
if(index2==-1) break;
temp=str.substring(index1+3, index2);
System.out.println("temp:"+temp);
i=index2;
}
}
if(str.equals("0")&&type.equals("CheckToken")) {
output ="3";
output += View.UploadMenuView();
out_bytes=new byte[10000];
out_bytes=output.getBytes();
out.write(out_bytes);
out.flush();
System.out.println("ѷϢ~:"+output);
//System.out.println("ѷϢ~:"+out_bytes);
str=in.readUTF();
System.out.println("ӷյ֤"+str);
}
//0ʾעɹ,1ʾֻظ2ʾԿظ
}catch(Exception e) {
e.printStackTrace();
}
//}
}catch(IOException e) {
e.printStackTrace();
}finally {
System.out.println("ͻߣͷԴ");
//ͷԴ
if(null!=out) {
try {
out.close();
}catch(IOException e) {
e.printStackTrace();
}
}
if(null!=in) {
try {
in.close();
}catch(IOException e) {
e.printStackTrace();
}
}
if(null!=socket) {
try {
socket.close();
}catch(IOException e) {
e.printStackTrace();
}
}
if(null!=sc) {
sc.close();
}
}
}
}
private static String MenuView() {
while(true) {
String input=null;
switch(item) {
case 0://˳
System.exit(-1);
break;
case 1://עû
input= View.loginUserMenuView();
return input;
case 2://ûϴtoken
input=View.CheckTokenMenuView();
return input;
case 3://ҽԱtoken
input= View.ApplyTokenMenuView();
return input;
case 4://ԶϴMatching_key
input=View.AutoUploadView();
return input;
case 5:
input="5";
return input;
case 6:
input="6";
return input;
default:
break;
}
}
}
}
================================================
FILE: Server/src/com/controller/PioneerServer.java
================================================
package com.controller;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import com.bean.User;
import com.bean.UserMessage;
import com.dao.TaskThread;
import com.dao.UserDao_Imp;
//HTTPSServerʵ
public class PioneerServer {
private int port = 446;
private boolean isServerDone = false;
public static void main(String[] args) throws IOException {
PioneerServer server = new PioneerServer();
server.run();
}
public PioneerServer() {
}
PioneerServer(int port) {
this.port = port;
}
// ʼSSLContext
public SSLContext createSSLContext() {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("./server.p12"), "010320".toCharArray());
// Լʵιָ֤࣬
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, "010320".toCharArray());
KeyManager[] km = keyManagerFactory.getKeyManagers();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(keyStore);
TrustManager[] tm = trustManagerFactory.getTrustManagers();
// Initialize SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(km, tm, null);
return sslContext;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
// ʼ
@SuppressWarnings({ "resource", "null" })
public void run() throws IOException {
try {
SSLContext sslContext = this.createSSLContext();
// socket factory
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
// ServerSocket͵Ķṩ˿ں
SSLServerSocket serverSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(this.port,3);
System.out.println("This is Pioneer Server!");
System.out.println("Waiting for the client to connect......");
System.out.println("SSL server started");
// ûÿͻʱaccept
new TaskThread().start();
ExecutorService fixedThreadPool=Executors.newFixedThreadPool(3);
while(!isServerDone){
SSLSocket socket = (SSLSocket) serverSocket.accept();
// ̳߳صʵ
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("߳ " + Thread.currentThread().getName() + " ");
try {
new ServerThread(socket).run();
}catch(Exception e) {
}finally {
if(Thread.currentThread().isInterrupted()) {
Thread.currentThread().stop();
}
System.out.println("finally:"+Thread.currentThread().getName() + "stop");
}
}
});
}
}catch (Exception ex){
ex.printStackTrace();
}
}
// ̴߳
static class ServerThread {
private SSLSocket serverSocket = null;
final int readTimeout=60*1000;
final int connectTimeout=10*1000;
UserMessage userMessage = null;
ServerThread(SSLSocket sslSocket){
this.serverSocket = sslSocket;
}
@SuppressWarnings("null")
public void run() {
DataInputStream in=null;
DataOutputStream out=null;
try {
serverSocket.setSoTimeout(connectTimeout);//ӳʱ
serverSocket.setEnabledCipherSuites(serverSocket.getSupportedCipherSuites());
// ֹ
serverSocket.startHandshake();
serverSocket.setSoTimeout(readTimeout);
// Ựȡ
SSLSession sslSession = serverSocket.getSession();
System.out.println("SSLSession :");
System.out.println("\tProtocol : " + sslSession.getProtocol());
System.out.println("\tCipher suite : " + sslSession.getCipherSuite());
// ɹӵĿͻ˵ַ
System.out.println("ͻ" + serverSocket.getInetAddress() + "ӳɹ");
in=new DataInputStream(serverSocket.getInputStream());
out = new DataOutputStream(serverSocket.getOutputStream());
out.flush();
String token=null;
while (true) {
//ִй
byte[] bytes=new byte[10000];
User user = new User();
String tag=null;
String str="";
UserDao_Imp userDao_Imp = new UserDao_Imp();
int input=0;
//ȡͻ˷͵
do {
input = in.read(bytes);
str=new String(bytes);
if(str.substring(0,1).equals("5")||str.substring(0,1).equals("6")) {break;}
}while(input<=1);
if(input!=0) {
String ini_str="";
str="";
ini_str=new String(bytes);
for(int i=0;i0?true:false;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return false;
}
@Override
public boolean delete(User user) {
//
//Ӷ
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_DELETE);
//sqlIJ
prepareStatement.setString(1,user.getKey());
int line = prepareStatement.executeUpdate();
return line>0?true:false;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return false;
}
@Override
public boolean select_users_key(User user) {
//
//Ӷ
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_SELECT_users_ID);
//sqlIJ
prepareStatement.setString(1,user.getKey());
ResultSet result = prepareStatement.executeQuery();
while(result.next()) {
String uID=result.getString(1);
if(uID!=null) {return false;}
else {return true;}
}
} catch (Exception e) {
return false;
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return true ;
}
@Override
public String loginUser(User user) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
boolean flag=select_users_ID(new User(user.getId(),user.getKey()));
if(!flag) {return "1";}
flag=select_users_key(new User(user.getId(),user.getKey()));
if(!flag) {return "2";}
prepareStatement = conn.prepareStatement(SQL_USER_LOGIN_User);
//sqlIJ
prepareStatement.setString(1, user.getId());
prepareStatement.setString(2, user.getKey());
prepareStatement.executeUpdate();
return "0" ;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "2";
}
public boolean loginManager(User user) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_LOGIN_Manager);
//sqlIJ
prepareStatement.setString(1, user.getUsername());
prepareStatement.setString(2, user.getPassword());
prepareStatement.executeUpdate();
return true ;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return true;
}
public boolean insert_token(User user,String token) {
//
//Ӷ
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_INSERT_token);
prepareStatement.setString(1, user.getUsername());
prepareStatement.setString(2, token);
int line = prepareStatement.executeUpdate();
return line>0?true:false;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return false;
}
public String ApplyToken(User user) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
/* ע˺
User user0=new User(); user0.setUsername(user.getUsername());
user0.setPassword(user.getPassword()); loginManager(user0);*/
prepareStatement = conn.prepareStatement(SQL_USER_SELECT_managers_key);
//sqlIJ
prepareStatement.setString(1, user.getUsername());
ResultSet result = prepareStatement.executeQuery();
while(result.next()) {
String uKey=result.getString(1);
if(uKey!=null) {
System.out.println("uKey"+uKey.length());
System.out.println("passwd"+user.getPassword().length());
boolean flag=uKey.equals(user.getPassword());
if(flag) {
System.out.println("ûЧ");
String token=Generate_token().toString();
System.out.println("TokenΪ:"+token);
insert_token(user,token);
return token;
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1";
}
public String CheckToken(User user) {
//
//Ӷ
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_CHECK_token);
//sqlIJ
prepareStatement.setString(1,user.getToken());
System.out.println("token_user"+user.getToken());
System.out.println("token_len"+user.getToken().length());
ResultSet result = prepareStatement.executeQuery();
while(result.next()) {
System.out.println(result.getString(1));
String uname=result.getString(1);
System.out.println("uname"+uname);
if(uname!=null) {return "0";}
else{return "1";}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1" ;
}
public void UploadMatching_key(User user) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_UPLOAD_Matching_key);
//sqlIJ
for(int i=1;i<15;i++) {
prepareStatement.setString(i,user.getMatchingKeys(i-1));
System.out.println(user.getMatchingKeys(i-1));
}
prepareStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
}
public boolean DeleteToken(String token) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement=conn.prepareStatement(SQL_USER_DELETE_token);
System.out.println(token);
prepareStatement.setString(1, token);
int line=prepareStatement.executeUpdate();
return line>0?true:false;
}catch(Exception e){
e.printStackTrace();
return false;
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
}
public String Upload(User user,String token) {
// uploadһֶ
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_UPLOAD_Matching_keys);
//sqlIJ
prepareStatement.setString(1,user.getId());
prepareStatement.setString(2,user.getMatching_keys());
System.out.println("Matching_keys:"+user.getMatching_keys());
System.out.println("ϴid"+user.getId().length());
System.out.println("ϴkey"+user.getMatching_keys().length());
prepareStatement.executeUpdate();
if(!token.equals("123456")) {
//һtokenɾ
if(DeleteToken(token)) { System.out.println("delete token"); }
}
return "0" ;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1";
}
public String AutoUpload(User user) {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
try {
prepareStatement = conn.prepareStatement(SQL_USER_AUTOUPLOAD_Matching_keys);
//sqlIJ
prepareStatement.setString(1,user.getId());
prepareStatement.setString(2,user.getMatching_keys());
System.out.println("userID:"+user.getId());
System.out.println("Matching_keys"+user.getMatching_keys());
prepareStatement.executeUpdate();
return "0" ;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1";
}
public String Update_Infectedusers() {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
PreparedStatement prepareStatement2=null;
PreparedStatement prepareStatement3=null;
try {
String ID="";
String Matching_keys="";
String Update_keys="";
//sqlIJ
Statement statement=conn.createStatement();
ResultSet result=statement.executeQuery(SQL_Select_Infectedusers);
while(result.next()) {
ID=result.getString(1); //õһеID
Matching_keys=result.getString(2); //õһеMatching_keys
if(Matching_keys.length()<=Matching_key_length) {
System.out.println("Delete_Infected_ID:"+ID);
System.out.println("Delete_Infected_Matching_key:"+Update_keys);
prepareStatement3=conn.prepareStatement(SQL_Delete_Infectedusers);
prepareStatement3.setString(1,ID);
prepareStatement3.executeUpdate();
}else {
Update_keys=Matching_keys.substring(Matching_key_length); //ÿǰ3λȡ
System.out.println("Infected_ID:"+ID);
System.out.println("Infected_Matching_key:"+Update_keys);
prepareStatement2=conn.prepareStatement(SQL_Update_Infectedusers);
prepareStatement2.setString(1, Update_keys);
prepareStatement2.setString(2, ID);
prepareStatement2.executeUpdate();
}
}
return "0";
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1";
}
public String Update_Contactedusers() {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
PreparedStatement prepareStatement2=null;
PreparedStatement prepareStatement3=null;
try {
String ID="";
String Matching_keys="";
String Update_keys="";
Statement statement=conn.createStatement();
ResultSet result=statement.executeQuery(SQL_Select_Contactedusers);
while(result.next()) {
ID=result.getString(1); //õһеID
Matching_keys=result.getString(2); //õһеMatching_keys
if(Matching_keys.length()<=Matching_key_length) {
System.out.println("Delete_Contacted_ID:"+ID);
System.out.println("Delete_Contacted_Matching_key:"+Update_keys);
prepareStatement3=conn.prepareStatement(SQL_Delete_Contactedusers);
prepareStatement3.setString(1,ID);
prepareStatement3.executeUpdate();
}else {
Update_keys=Matching_keys.substring(Matching_key_length); //ÿǰ3λȡ
System.out.println("Contacted_ID:"+ID);
System.out.println("Contacted_Matching_key:"+Update_keys);
prepareStatement2=conn.prepareStatement(SQL_Update_Contactedusers);
prepareStatement2.setString(1, Update_keys);
prepareStatement2.setString(2, ID);
prepareStatement2.executeUpdate();
}
}
return "0";
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn, prepareStatement, null);
}
return "1";
}
public String Download() {
Connection conn=JDBCUtils.getConnection();
PreparedStatement prepareStatement=null;
String Matching_keys="",Line_Matching_keys;
try {
prepareStatement = conn.prepareStatement(SQL_SELECT_AES_Matching_keys);
ResultSet result= prepareStatement.executeQuery();
while(result.next()) {
Line_Matching_keys=result.getString(1); //ѯAES_HEXܺݣصһַ
Matching_keys +="ABC"+Line_Matching_keys;
}
Matching_keys +="ABC";
return Matching_keys;
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtils.close(conn,prepareStatement,null);
}
return null;
}
}
================================================
FILE: Server/src/com/jdbc/JDBCUtils.java
================================================
package com.jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
//װJDBCݿ
public class JDBCUtils {
private static String driver;
private static String url;
private static String username;
private static String password;
static {
//ʽgetClassLoader
InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream("db.properties");
//property Ķ
Properties p = new Properties();
//ļ
try {
p.load(is);
driver = p.getProperty("driver");
url = p.getProperty("url");
username = p.getProperty("username");
password = p.getProperty("password");
//MySql
Class.forName(driver);
System.out.println("سɹ");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace() ;
}
}
//Ӷ
public static Connection getConnection() {
try {
System.out.println("ݿӳɹ");
return DriverManager.getConnection(url, username, password);
}catch(SQLException e) {
System.out.println("ݿʧ");
e.printStackTrace();
}
return null;
}
//ͷԴ
public static void close(Connection conn,Statement statement,ResultSet result) {
try {
if(result!=null) {
result.close();
result=null;
}
if(statement!=null) {
statement.close();
statement=null;
}
if(conn!=null) {
conn.close();
conn=null;
}
}catch(Exception ex) {
ex.printStackTrace();
}
}
}
================================================
FILE: Server/src/db.properties
================================================
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/Pioneer
username=root
password=Pioner+6
================================================
FILE: Server_jar/META-INF/MANIFEST.MF
================================================
Manifest-Version: 1.0
Main-Class: org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader
Rsrc-Class-Path: ./ mysql-connector-java-5.1.49-bin.jar org.junit_4.13.0
.v20200204-1500.jar org.hamcrest.core_1.3.0.v20180420-1519.jar
Rsrc-Main-Class: com.controller.PioneerServer
Class-Path: .
================================================
FILE: Server_jar/db.properties
================================================
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/Pioneer
username=root
password=Pioner+6
================================================
FILE: Test/Contribution.md
================================================
### 创新性说明
#### 短暂连接传输的蓝牙数据交互方式
1、payload 长度的要求
蓝牙广播包的方式限制了 payload 的长度范围,使得传输数据的长度受到限制。
对于一些 Receive Only 的设备,其它设备无法读取其 payload,此时通过数据的读写来实现传输,并在内部实现了对分片传播的处理,使得最大的可传输数据长度变为 512 字节。因此需要通过建立短暂蓝牙连接的方式写入数据。
2、广播包的不安全性
广播包的时候其他的任意设备都可以收到发送出去的 payload,因此处于安全性考虑,不采用这种方式发送数据包。
3、蓝牙连接提高安全性
通过蓝牙之间建立短暂连接的方式去交互,可以确保收到 payload的设备必然安装本 Pioneer APP,此时发送数据包提高安全性。
4、多设备交互
通过短暂连接传输交互数据一次同时连接多个设备,连接效果效果稳定。
#### 抗重放攻击 MAC
在传统的 payload 的基础上添加了有效期的限制和 MAC 的认证,从而提高了系统对于重放攻击的防御能力。
1、有效期验证
在收到数据包的时候,先去检测收到时间是否在数据包有效期之内,如果在有效期之内则收下,否则丢弃数据包。
2、MAC 验证
MAC 是在 APP 下载了感染者信息,与本地保存的临时标识符去匹配。MAC 是由 payload 的四项和当天的 Matching Key 通过 HMAC-SHA256生成的。
在匹配成功的情况下,则去验证 MAC,验证通过方可说明该用户是密切接触者,则后台自动上传该用户近 14 天的 Matching Keys。否则,认为该数据包是被伪造的。

#### 去中心化模式
采用去中心化的方式,最大程度的保护了用户的隐私,同时又保证了中心服务器对于感染者和密切接触者的精确追踪。
1、中心化模式
传统的追踪系统是把数据上传到中心服务器,由中心服务器保存信息,并直接在服务器端计算匹配。
2、去中心化模式
本系统采用去中心化的方式,用户将接收到的临时身份标识符保存到本地,将计算匹配接触者信息的工作放到手机本地。
在该用户没有接触感染者的情况下,中心服务器不会知道关于该用户的任何的接触信息。即使该用户称为了密切接触者,别人也无法获悉该用户的任何隐私信息,也就无法知道该用户的身份。
#### 密码学技术
1、临时信息和密钥的 Hash 链式生成
采取以 SM3 算法的方式,以 Secret key 生成 Matching keys 以及生成 contanctkey,由 Secret key 生成 Contact identifier 的过程中涉及到了伪随机数生成器 (PRNG) 和伪随机函数 (PRNF),前者用于生成伪随机数,后者用于将分布可能不均匀的输入空间映射到分布均匀的输出空间。

简要流程如下

详细流程如下:
在用户第一次打开 app 的时候,app 内部会自动调用伪随机数生生成器(PRNG)生成一个 Secret key 与用户的手机号进行绑定,并提交到服务器:

接下来由 Secret key 可以生成一系列的 matching seeds。若打算提交生成 n 个 Contact identifier,则 Matching seeds 由以下递推式生成:

此处 Truncate 函数表示截取输入一半的长度。
matching seeds 用于生成 matching keys,生成关系式如下:

Maching key 用于生成每一天的 Contact key,Contact key 从Matching key 的生成过程,与 Maching key 从 Secret key 生成的过程完全相同。
最后,我们得到了一系列 Contact key,Contact key 用于生成Contact identifier,Contact identifier 是最终用于蓝牙数据交互的临时用户标识符:

2、国密算法 SM3
SM3 杂凑算法是我国自主设计的密码杂凑算法,适用于商用密码应用中的数字签名和验证消息认证码的生成与验证以及随机数的生成,可满足多种密码应用的安全需求。为了保证杂凑算法的安全性,其产生的杂凑值的长度不应太短,SM3 算法的输出长度为 256 比特,有很好的安全性。
我们通过 SM3 国密 hash 算法,将 SM3 运用到 contact identifier的链式生成结构上。
在 Secret key 的生成过程中,我们通过 SM3 生成基于 hash 函数的随机数生成器,从而生成具有密码学安全性质的伪随机 Secret key。在 payload 中添加 MAC 来保证 payload 的认证性和完整性时,我们通过将 SM3 运用于 HMAC 模式,生成 MAC。
### 总结
我们参考现有的追踪交互协议,如 Opentrace 协议,并在此基础上,设计出了一套成熟的基于 BLE 的感染者追踪系统,以服务器作为信息交换的桥梁,客户端不同移动设备操作系统间的交互作为接触信息的基础,以此建立系统。
为了增强系统客户端的普适性,我们在Android 和 IOS 都进行了设计,分别写出了可行的 app,并且进行了蓝牙 BLE 交互的测试,针对测试中出现的部分问题,对代码进行了进一步的优化。
最后给出了现有的两款针对不同系统的 app 客户端。
然后基于服务器 HTTPS 连接,我们进行了不同设备之间的匹配测试,确保在接触感染者后可以确保给出接触提醒。
在上述设计实现之后,我们给出了基于本系统可能出现的各种情况下的数据测试,例如在外部大量蓝牙设备干扰的情况下确保连接的稳定性,测试在大量数据下确保匹配的精确性,在外部重放攻击的情况下的抵抗情况等等。
================================================
FILE: Test/README.md
================================================
### Pioneer for iOS 测试
Pioneer for iOS 的目的是设计出耗电量低、CPU 占用低、存储占用低的能够在后台持续运行的、适用范围广的联系人跟踪应用。该测试模块基于 Pioneer for iOS 在 iPhone 上的测试数据,给出 Pioneer 应用在正常的工作模式下的功耗、内存使用情况、CPU 占用量、网络使用量等数据。这不仅能使开发人员对 Pioneer 进行更细致的优化,同时向公众公开测试数据,以提高对 Pioneer for iOS 的信任。
实验时,在正常的工作模式下使用 Xcode 等工具测试设备 iPhoneXS 的各项测试标准,同时周围共构建 10 台 Android 设备和 iOS 设备以模拟正常的交互场景。
#### 功耗报告
基于低功耗蓝牙(BLE)的技术,Pioneer for iOS 最主要的测试标准之一是有关功耗的问题。

应用在前台时,主要的耗电量来自于 Display,而 CPU 耗电却较少。当应用在后台时,耗电量也是低的,即无论是前台还是后台 Pioneer foriOS 的耗电等级就系统评估来讲都是 Low 级别的,这正与 Pioneer 的设计初衷相符。
此外,当开启了位置传感器这一 Pioneer 可选功能时,前台和后台的耗电量都将因为位置传感器的持续工作而增大(这并没有暴露用户隐私),这对于某些人群来说可能使无法接受的,因此 Pioneer 将这一功能设置为可选功能并在持续进行优化,尽量使耗电量更少。
#### CPU 报告

正常工作模式下,Pioneer 的 CPU 占用率非常低,而且对 CPU 的占用并不是连续的。测试中 Pioneer 的 CPU 占用峰值为 13%,但通常平均占用一般是 2~6%。

Pioneer for iOS 对网络的使用主要是与 Pioneer 服务器建立连接并交互信息,但这并不是频繁的,换句话说 Pioneer 应用只会定期(至少每天一次)与服务器建立连接并交互信息:
#### 用户注册
用户注册需要与服务器连接并注册信息,上面给出了用户注册时的网络使用情况。用户的注册只在应用首次使用时才进行,也就是说这种连接在整个应用生命周期内只出现一次。
#### 感染者信息下载
另一个最主要的功能就是 Pioneer 定期从服务器下载感染者信息,此时网络的使用量就不再是固定统一的了,因为这与服务器中感染者的安全身份信息量有关。
#### 上传自身安全信息
当用户被确诊后或者在 Pioneer 内部的安全检测中被检测出与风险者有过接触而成为风险者后,Pioneer 将上传用户的近 14 天安全身份信息至 Pioneer 服务器,此时与服务器进行的网络交互数据量是固定的。
总地来说,Pioneer for iOS 在正常的用户身份信息交互阶段并不使用网络,而仅仅在与 Pioneer 服务器交互安全数据时才使用网络,并且这是少量的。
#### 内存报告

在测试期间内,Pioneer for iOS 的内存使用如上图所示,内存使用峰值可达 44.6MB,相对于系统来说仅占用了 1.2%,这是比较低的。经过多次测试(包括变换环境)发现应用对内存的使用均是较低的,这正符合我们的预期。
#### 后台可持续运行

实验测试使用标准设备(iPhone XS)进行测试,给出以上连续 8 个小时以上的测试数据。可以看到,Pioneer for iOS 在全部的 8 小时之内休眠时间仅占了 20 分钟,即 Pioneer 可以在后台达到 96% 的持续运行并且保证不被系统管理自动杀死,实际上这在正常的模拟使用环境中是可以达到较好的效果的。
#### 适用范围
在 Pioneer 开发中,已经尽量采用各种技术降低对部署设备的要求:
(1)用户身份信息的交互通过读写固定特征来实现,这给那些无法使用广播的设备(如三星 J6 等)提供了正常工作的机会。
(2)代码开发中不使用第三方库而使用系统推荐的库并且在保证安全性的前提下尽量使用对系统版本要求底的内置库。但是,基于 Pioneer中对 Network 库的使用以保证安全的 TLS 连接,Pioneer for iOS 对系统的要求最低为 iOS12.1。
从上面的数据可以看到,当今 iPhone 设备中系统版本在 iOS 12 以上的设备占有率至少可以达到 94%,因此 Pioneer for iOS 的至少可支持当前市场 iPhone 设备中近 94% 的设备。对于 iOS-iOS,Pioneer 可达到 94% × 94% = 88.36% 的部署覆盖率。
### Pioneer for Android 测试
Pioneer for Android 的目的是设计出耗电量低、CPU 占用低、存储占用低的能够在后台持续运行的、适用范围广的联系人跟踪应用。该测试模块基于 Pioneer for Android 在 HUAWEI-P40 ELS-AN00 以及 HUAWEI nova5 pro SEA-AL10 上的测试数据,给出 Pioneer 应用在正常的工作模式下的功耗、内存使用情况、CPU 占用量、网络使用量等数据。给出上述的测试分析,有助于我们后续对应用的优化,以及去判断应用的普适性,提高开发者对 Pioneer 的信任。
实验时,在正常的工作模式下,我们采取 HUAWEI 手机系统的方式来测试能耗信息,利用腾讯 WeTest 平台下的 Perfdog 工具对相应数据进行测试分析,同时周围共构建 10 台 Android 设备和 iOS 设备以模拟正常的交互场景。
#### 功耗报告
基于低功耗蓝牙(BLE)的技术,Pioneer for Android 最主要考虑的问题便是能耗问题,我们基于系统后台的电量测试给出相应的分析。
测试设备:华为 P40 Pro
测试环境:测试手机蓝牙能够探测到的范围内有 20 个左右蓝牙外设,其中安装本 app 的有 8 台设备。
测试时间:10 小时
具体数据如下图所示

应用在前台运行时,其电量消耗主要是因为 UI 展示和数据的更新,后台能耗主要是消耗在定时的广播、扫描与连接和定时的网络任务,经过我们的测试,前台每小时耗电量约为 100mAh,在手机中的耗电比重大概每小时 2.5%,APP 在后台运行总时长为 10 小时 18 分 48 秒,耗电量为 474.28mAh,平均每小时耗电量为 46mAh,在手机中的耗电比重大概为1%,实现了低能耗的设计初衷。
#### CPU 报告
正常工作模式下,Pioneer 的 CPU 占用率非常低,而且对 CPU 的占用并不是连续的。测试中 Pioneer 的 CPU 占用峰值为 10%,但大部分时间 CPU 占用率很低,平均占用率仅为 2%。

#### 网络报告
Pioneer for Android 对网络的使用主要是与 Pioneer 服务器建立连接并交互信息,但这并不是频繁的,换句话说 Pioneer 应用只会定期(至少每天一次)与服务器建立连接并交互信息,因此网络流量消耗在下面:
总地来说,Pioneer for Android 在正常的用户身份信息交互阶段并不使用网络,而仅仅在与 Pioneer 服务器交互安全数据时才使用网络,并且这是少量的。
#### 内存报告


在测试期间,Pioneer for Android 的内存使用如上图所示,内存使用峰值可达 61.0MB,相对于系统来说仅占用了 1.0%,这是比较低的。我们又进行了多次测试,平均占用内存在 50MB 左右,占用的内存很少,基本上达到了设计预期。
#### 后台可持续运行
后台保活目前暂时只采用了 Foreground Service 来降低 APP 在后台被杀的可能性。前台服务是那些被认为用户知道(用户所认可的)且在系统内存不足的时候不允许系统杀死的服务。前台服务会给状态栏提供一个通知,它被放到正在运行标题之下——这就意味着通知只有在这个服务被终止或从前台主动移除通知后才能被解除。将 Service 设置为前台服务,Service 可以一直保持运行状态且不会在内存不足的情况下被回收。
#### 同时连接多台设备
App 可以在同一时间连接多台设备进行数据交换。测试中,我们使用9 部手机同时运行 app,其中一部手机收到的数据包如下图所示:

从图中可以看到,每一个数据包的接收时间都是在 23:39——23:40之间,这说明 8 个数据包来自不同的手机,且在 1 分钟内全部建立连接并完成数据交互。由此可见,我们的 app 至少能够同时连接 8 台设备,这个数量对于小规模聚集来说已经足够大。
#### 适用范围
在 Pioneer 开发中,已经尽量采用各种技术降低对部署设备的要求:
1)用户身份信息的交互通过读写固定特征来实现,这使得那些无法作为蓝牙外设的设备(如三星 J6 等)也能够和其它用户正常交互数据。
2)代码开发中不使用第三方库而使用系统推荐的库并且在保证安全性和可用性的前提下尽量使用对系统版本要求底的内置库。但是,基于对蓝牙 BLE 的协议保证,我们设置的最低 SDK 版本为 Android 5.0,即 API21,Pioneer 对 API 21 以下的系统不能支持。

如上图,我们从 Android studio 官方得知,API21 及以上的 SDK能够适用全球 94.1% 的 Android 手机,因此我们知道,Android 之间Pioneer 交互的覆盖率为:94.1% × 94.1% = 88.55%,Android 和 iOSPioneer 交互的覆盖率为 94.1% × 94.1%=88.45%。