这是一遍记录DTSmartBattery 代码审核文件更改记录。
DTSmartBattery是吉毅创公司研发的蓝牙app,具有自主知识产权和专利产权,配合公司的电池设备使用。通过app连接公司的电池后可以查看电池的基本信息和电池的一些故障信息。

重构
精简代码,删除无效代码。按审核文档中标准修改.
添加对Build.VERSION_CODES.O(Android12+ )SDK的适配
//Android 12 + 需要的运行权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MainActivityPermissionsDispatcher.requestAndroid12AboveBluetoothPermissionsWithPermissionCheck(this);
}else {
MainActivityPermissionsDispatcher.requestAndroid11BelowBluetoothPermissionsWithPermissionCheck(this);
}
/**
* android 12+ 蓝牙权限
*/
@NeedsPermission({Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.WRITE_EXTERNAL_STORAGE})
void requestAndroid12AboveBluetoothPermissions(){
initTab();
}
/**
* android 11- 蓝牙权限
*/
@NeedsPermission({Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.WRITE_EXTERNAL_STORAGE})
void requestAndroid11BelowBluetoothPermissions(){
initTab();
}
添加注释
/**
* 修复连上设备后有些设备获取不到数据,如果上一次连上了设备,且成功获取到了数据,需要清空当前页面上一次的UI数据
* @param event
*/
@Subscribe
public void cutHomeNavigation(HomeNavigationEvent event) {
switch (event.getHomeNavigat()) {
case DEVICE_DETAIL:
resetUI();
break;
}
所有被赋值的变量赋值类型是否一致或有类型转换
所有代码在使用完后会在适当的时机清空还原。
/**
* 切换连接设备重置标记数据
*/
private void resetData(){
MyApplication.BMS_VERSION = "";
MyApplication.HARDWARE_BMS_VERSION = "";
MyApplication.IS_NEW_DEVICES = false;
MyApplication.IS_SUPPORT_WIFI = false;
MyApplication.IS_DEVICE_48V = false;
MyApplication.IS_OFFLIN = false;
isClose = false;
}
OTA升级逻辑代码的重构
优化了蓝牙升级的逻辑,添加了更多的注释,使以后能清晰明了的知道蓝牙升级的整个流程。
if (result.contains("A6") || result.contains("A7") || result.contains("A8") || result.contains("A9") || result.contains("01") || result.contains("00")) { //2022-09-23测试的是时候发现会分段分收到升级的数据。比如第一段只收到A6A7A8 第二段收到A9后面的。故只能拼加
if (!sb.toString().startsWith("A6")) {
sb.delete(0, sb.length());
}
sb.append(result);
ViseLog.i("拼加之前的StringBuffer:" + sb.toString());
if (!sb.toString().startsWith(PREFIX)) {
return;
}
ViseLog.i("拼加之后的StringBuffer:" + sb.toString() );
//1.截取从头到倒数第四个的字符串
//2. 通过 CRC验证截取的字符串
//3. 如果验证的CRC的值和 返回的数据最后四位CRC的数据相等,代表数据正常
//4. 如果验证通过后,再能过返回的数据,截取7-9位的数据,如果是01(成功), 00(不成功),成功进行发下一段数据,直到发完
// A6A7A8A90100015914 0x01成功,0x00不成功
String calcCRC = sb.substring(0, sb.length() - 4); //.通过 CRC验证截取的字符串,截取从头到倒数第四个的字符串
String resultCRC = sb.substring(sb.length() - 4);
String resultCode = sb.substring(8, 10);
//如果收到的是以A6A7A8A9 开头的,代表升级未完在
//较验CRC数据和返回的CRC数据相等
// if (StringUtils.getCRC2(decodeHex(calcCRC)).equalsIgnoreCase(resultCRC)) {
//CRC验证通过,验证返回的如果 是01 代表成功,发一段数据
if (sb.toString().startsWith(PREFIX)) {
if (resultCode.equalsIgnoreCase("01")) {
if (dataInfoQueue.peek() != null) {
retryCount = 0;
send(false, bluetoothLeDevice);
if (dataInfoQueue.size() == 0) { //如果队列为null。代表数据全部发送完成 ,直接提示升级完成
// mHandler.obtainMessage(BleOtaUpdater.OTA_OVER, packID).sendToTarget();
ViseLog.i("队列为空....");
} else {
mHandler.obtainMessage(BleOtaUpdater.OTA_UPDATE, packID).sendToTarget();
}
sb.delete(0, sb.length());
ViseLog.i("发送完:" + packID + "段数据, 数据内容是:" + currentSendData);
return;
}else if (dataInfoQueue.size() == 0){
ViseLog.i("队列为空....");
sb.delete(0, sb.length());
mHandler.obtainMessage(BleOtaUpdater.OTA_OVER, packID).sendToTarget();
ViseLog.i("发送完成:" + result);
return;
}
}
} else {
SystemClock.sleep(200);
if (dataInfoQueue.peek() != null || !dataInfoQueue.isEmpty()) {
retryCount = 0;
send(false, bluetoothLeDevice);
//研发那边说,不需要等他返回F6F7F8F2 开头命令,发送完,直接显示升级已经完成
if (dataInfoQueue.size() == 0) { //如果队列为null。代表数据全部发送完成 ,直接提示升级完成
// packID = 0;
mHandler.obtainMessage(BleOtaUpdater.OTA_OVER, packID).sendToTarget();
ViseLog.e("队列为空....");
} else {
mHandler.obtainMessage(BleOtaUpdater.OTA_UPDATE, packID).sendToTarget();
}
sb.delete(0, sb.length());
ViseLog.i("发送完:" + packID + "段数据, 数据内容是:" + currentSendData);
return;
}
}
工厂模式构建Dialog
解耦:把对象的创建和使用的过程分开, 降低代码重复: 如果创建某个对象的过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。降低维护成本:由于创建过程都由工厂统一管理,所以发生业务逻辑变化,不需要找到所有需要创建某个对象的地方去逐个修正,只需要在工厂里修改即可,降低维护成本。
public abstract class BaseDialog extends Dialog {
protected ProgressDialog progressDialog;
protected Context mContext;
public BaseDialog(@NonNull Context context) {
super(context, R.style.dialog_style);
this.mContext = context;
}
protected abstract void init();
protected abstract int layoutId();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(layoutId());
ButterKnife.bind(this);
//设置dialog的宽(宽=屏幕宽-40),高为自适应
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = getWindow().getWindowManager().getDefaultDisplay().getWidth()-20;
// params.height = WindowManager.LayoutParams.WRAP_CONTENT; ;
getWindow().setAttributes(params);
init();
}
在子类中是放到父类中的通用成员
在父类中声明一些子类需要共同用的到的些变量,降低代码重复。
/**
* 解析数据
* @param <T>
*/
public abstract class BaseBluetoothDataParse<T extends BaseBean> {
/**
* 上传服务器对象
*/
protected T data ;
/**
* 蓝牙发送的原始字符串,主要做显示用
*/
protected String dataStr ;
public abstract T parseData(String data);
/**
* 重置数据
*/
public void resetData(){
data = null;
dataStr = "";
}
所有蓝牙命令声明在一个接口类里面
所有跟电池设备发送的指令,和配置的UUID都在一个类里面,降低维护成本。
/**
* 蓝牙设备指令
*/
public interface BluetoothCommand {
/**
* 设备历史故障1(总共60条,每个命令返回10条)
*/
String HISTORY_RECORD_COMMAND_1 = "DDA50700FFFA77";
String HISTORY_RECORD_COMMAND_2 = "DDA51700FFFA77";
String HISTORY_RECORD_COMMAND_3 = "DDA52700FFFA77";
String HISTORY_RECORD_COMMAND_4 = "DDA53700FFFA77";
String HISTORY_RECORD_COMMAND_5 = "DDA54700FFFA77";
String HISTORY_RECORD_COMMAND_6 = "DDA55700FFFA77";
/**
* About Fragment界面的蓝牙命令
* 查询设备版本号,BMS版本号,设备日期
*/
String VISON_COMMAND = "DDA50500FFFB77";
/**
* 新设备发这个指令
* wifi 连接查看电压图表信息指令
*/
String VOLTAGE_INFO_COMMAND = "DDA50800FFFA77";
/**
* wifi 连接查看电压图表信息指令
* 旧设备发此指令
*/
String VOLTAGE_INFO_COMMAND_OLD = "DDA50400F9F977";
/**
* 蓝牙设备连接后,详情指令(温度状态,电压状态)
*/
String DEVICE_INFO_COMMAND = "DDA50300FBF377";
/**
* 发送WIFI账号和密码的按键,给WIFI配网,用于和充电器上的WIFI模块通讯。
*/
String WIFI_ACCOUNT_INFO_COMMAND = "DDA50C%sFFFA77";
/**
* 设备wifi连接指令
*/
String WIFI_CONNECT_COMMAND = "DDA50900FFFA77";
/**
* 电池健康信息
*/
String BATTERY_HEALTH_COMMAND = "DDA50600FFFA77";
/**
* BMS系统时间设置按键(点击一次发送一次)
*/
String BMS_SET_TIME_COMMAND = "DDA50A00%sFFFA77";
/**
* BMS系统时间查询按键(点击一次发送一次
*/
String BMS_GET_TIME_COMMAND = "DDA51A00FFFA77";
/**
* 关闭wifi联网(点击一次发送一次)
*/
String CLOSE_WIFI_CONNECT_COMMAND = "DDA50B00FFFA77";
/**
* 停止wifi配网(点击一次发送一次)
*/
String STOP_DISTRIBUTION_NETWORK_COMMAND = "DDA51900FFFA77";
/**
* 控制充电继电器(点击一次发送一次) 控制指令:0x5E:断开 0x5A:闭合
*/
String DEVICE_CHARGE_CONTROL_COMMAND = "DDA50D%sFFFA77";
/**
* 更改蓝牙编号(点击一次发送一次)
*/
String DEVICE_UPDATE_NO_COMMAND = "DDA50E%s7A7B77";
/**
* 是否充许BMS升级
*/
String BMS_UPDATE_COMMAND = "F6F7F8F1%s";
/**
* 高温低温记录
*/
String TEMPERATURE_RECORD_COMMAND = "DDA51200F9F977";
/**
* 累计使用情况
*/
String USER_RECORD_COMMAND = "DDA51300F9F977";
/**
* 温度和故障
*/
String TEMP_FAULT_COMMAND = "DDA51400F9F977";
/**
* 蓝牙设备的Service的UUID
*/
UUID UUID_SERVICE = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb");
/**
* 蓝牙设备的Characteristic的UUID
*/
UUID UUID_NOTIFY = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
}
使用服务来运行收集数据任务
APP 连上设备后需要收集电池设备的数据,启动一个服务在后台进行收集任务。优化用户体验。
CollectServiceIntentService extends IntentService {
public static final String CHANNEL_ID_STRING = "service_01";
// IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS
private static final String ACTION_FOO = "com.jiyic.smartbattery.collect.action.FOO";
private static final String ACTION_BAZ = "com.jiyic.smartbattery.collect.action.BAZ";
private static final String EXTRA_PARAM1 = "com.jiyic.smartbattery.collect.extra.PARAM1";
private static final String EXTRA_PARAM2 = "com.jiyic.smartbattery.collect.extra.PARAM2";
@SuppressLint("WrongConstant")
@Override
public void onCreate() {
super.onCreate();
NotificationManager notificationManager = (NotificationManager) MyApplication.getInstance().getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel mChannel = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
mChannel = new NotificationChannel(CHANNEL_ID_STRING, getString(R.string.app_name),NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(mChannel);
Notification notification = new Notification.Builder(getApplicationContext(), CHANNEL_ID_STRING).build();
startForeground(1, notification);
}
}
public CollectServiceIntentService() {
super("CollectServiceIntentService");
}
/**
* Starts this service to perform action Foo with the given parameters. If
* the service is already performing a task this action will be queued.
* 执行收集数据服务...
* @see IntentService
*/
public static void startActionFoo(Context context, String param1, String param2) {
Intent intent = new Intent(context, CollectServiceIntentService.class);
intent.setAction(ACTION_FOO);
intent.putExtra(EXTRA_PARAM1, param1);
intent.putExtra(EXTRA_PARAM2, param2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
/**
* Starts this service to perform action Baz with the given parameters. If
* the service is already performing a task this action will be queued.
* 动态替换当前页面指令。可以是否挂起。一般成对出现。替换后记得还原数据现场
* @param hangup 是否挂起,不执行收集任务(有些界面不能一直发指令,比如BMS升级功能。此时需要挂起任务不执行收集任务)
* @see IntentService
*/
public static void startActionBaz(Context context, String param1, boolean hangup) {
Intent intent = new Intent(context, CollectServiceIntentService.class);
intent.setAction(ACTION_BAZ);
intent.putExtra(EXTRA_PARAM1, param1);
intent.putExtra(EXTRA_PARAM2, hangup);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_FOO.equals(action)) {
final String param1 = intent.getStringExtra(EXTRA_PARAM1);
final String param2 = intent.getStringExtra(EXTRA_PARAM2);
handleActionFoo(param1, param2);
} else if (ACTION_BAZ.equals(action)) {
final String param1 = intent.getStringExtra(EXTRA_PARAM1);
final boolean param2 = intent.getBooleanExtra(EXTRA_PARAM2, false);
handleActionBaz(param1, param2);
}
}
}
/**
* Handle action Foo in the provided background thread with the provided
* parameters.
*/
private void handleActionFoo(String param1, String param2) {
// 收集数据
CollectManagerTask.getInstance().startCollectInfo(this);
}
/**
* Handle action Baz in the provided background thread with the provided
* 动态替换当前页面指令。可以是否挂起
* @param hangup 是否挂起,不执行收集任务(有些界面不能一直发指令,比如BMS升级功能。此时需要挂起任务不执行收集任务)
* parameters.
*/
private void handleActionBaz(String param1, boolean hangup) {
// 动态替换掉当前页面的执行命令
if (!TextUtils.isEmpty(param1)) {
CollectManagerTask.getInstance().setPlaceholderCommand(param1);
}
CollectManagerTask.getInstance().initReciver();
CollectManagerTask.getInstance().setHangup(hangup);
}
}
通过配置来实现扫描设备时是否需要过滤
在打包的时候工厂那边经常需要 不使用蓝牙扫描过滤。通过添加配置文件只要更改过滤标记就能实现此功能。方便灵活配置
//白名单中数据不为空。需要看设备是否在白名单中
boolean isWhite = true;
if (!BuildConfig.LOG_DEBUG) {
if (RealmUtil.query(DeviceModel.class).size() > 0) {
if (!TextUtils.isEmpty(bluetoothLeDevice.getName()) && bluetoothLeDevice.getName().startsWith("DT")){
String tempName = bluetoothLeDevice.getName().replace("DT", "").replaceAll(" ", "");
isWhite = RealmUtil.isExists(DeviceModel.class, tempName);
}
}
}
if (bluetoothLeDevice != null &&
TextUtils.isEmpty(bluetoothLeDevice.getName()) &&
bluetoothLeDevice.getName() != null &&
bluetoothLeDevice.getName().startsWith("DT") &&
isWhite)
buildTypes {
debug {
// 显示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
signingConfig signingConfigs.debug
minifyEnabled false
zipAlignEnabled false
shrinkResources false
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false" //
//签名
signingConfig signingConfigs.release
zipAlignEnabled false
// 移除无用的resource文件
shrinkResources false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
自定义View
项目中自定义Title,优化布局,让界面有特定的显示风格、效果. 更容易扩展。
/**
* 自定义公共Title
*/
public class Title extends RelativeLayout {
private int padding;
private int margin;
private TextView titleView;
private ImageView leftButton;
private ImageView rightButton;
private ImageView rightButton2;//右边靠里面的图片
private int textColor;
private TextView rightText;
public Title(Context context) {
this(context,null);
}
public Title(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Title(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(Context context, AttributeSet attrs) {
if (getMinimumHeight() == 0) {
setMinimumHeight(DensityUtil.dip2px(context,50)); //设置Title最小高度
}
padding = DensityUtil.dip2px(context,8);
margin = DensityUtil.dip2px(context,8);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Title);
boolean hasDivider = a.getBoolean(R.styleable.Title_hasDivider, false);
if (hasDivider){
View view = new View(context);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 1);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
view.setBackgroundColor(ContextCompat.getColor(context,R.color.dividerColor));
addView(view,layoutParams);
}
setBackgroundColor(a.getColor(R.styleable.Title_titleBg, ContextCompat.getColor(context,R.color.colorPrimary)));
textColor = a.getColor(R.styleable.Title_textColor, ContextCompat.getColor(context, R.color.white));
a.recycle();
}
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A
是否有类似“RVMuse1”或“RVMuselist[0]”之类的内容而不是键入整个版本号。在任何时候,我们都会看到一个可能包含5个或更多ruby的列表,我们可以轻松地键入一个数字而不是X.X.X。这也有助于rvmgemset。 最佳答案 这在RVM2.0中是可能的=>https://docs.google.com/document/d/1xW9GeEpLOWPcddDg_hOPvK4oeLxJmU3Q5FiCNT7nTAc/edit?usp=sharing-知道链接的任何人都可以发表评论