// // MNQtTrainVC.m // jiaPei // // Created by EchoShacolee on 2018/4/3. // Copyright © 2018年 JCZ. All rights reserved. // /* 首次连接流程:app搜索到外设 连接发送指令kGetMNdevVersion获取终端版本。判断是否需要升级。 升级完成/不需要升级 app发送请求kGetDevNumAndStuCardOrder获取终端序列号, ---看UI吧。那就是整个流程 然后 过程人脸验证会根据 登录返回的 mnqFacePorcess 判断是否开启, */ #import "MNQtTrainVC.h" #import "ScanVC.h" #import #import //bd计算相关 #import "DES3Util.h" #import "MNQDev.h" #import #import "PrecautionsView.h" #import "StepImageView.h" #define BLE_SEND_MAX_LEN 20 #define BLE_SEND_PER_LEN 0x0384 //900(0x0384)==图片 static NSString * const mnTableName = @"mnPeriod"; static NSString * const kServiceUUID = @"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; static NSString * const kCharacteristicUUID_write = @"6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; static NSString * const kCharacteristicUUID_reade = @"6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; static NSString * const kGetDevNumAndStuCardOrder = @"be000383eded"; static NSString * const kTakePhotoOrder = @"be000384eded"; static NSString * const kGetPhotoData = @"be000497"; static NSString * const kGetMNdevVersion = @"be00038aeded"; @interface MNQtTrainVC () { NSDateFormatter *dateFormatter; NSMutableDictionary *minuteTrainDic;//分钟学时用参数 签到和断线重连时候初始化 分钟学时发送之前更新字典 NSMutableDictionary *gatherTrainDic;//汇总学时用参数 签到和断线重连时候初始化 分钟学时发送之前更新字典 并存本地 短线重连用来恢复计时 NSTimer *timer;//学时计时器(它的存在可以且已经有用于判断是否在计时中) NSInteger seconds;//学时计数 MNQDev *mnqDev;//模拟器 //过程验证 BOOL onProcessVerification;//是否正在过程验证 NSInteger processVerificationTime;//过程验证时间 == 0 未开启, >0 开启需要验证 NSInteger processVerificationMaxCount;//允许失败次数 自己yy了一下==0不限制,服务器应该不会返回0吧 目前设置的是3次 超过三次就app发起签退 NSInteger processVerificationFailureCount;//失败次数 NSInteger photoSeconds;//拍照时间 //学时相关 NSString *subject; //当前训练科目 NSString *classId; //课堂ID NSString *classCode; //课程编码 NSString *trainNum; //学时编号 NSString *trainOrderNum;//学时编号用序列 每天置0 NSArray *minuteTrainArray;//分钟学时数组 用于汇总学时时候 获取本地库未上传的分钟学时 NSMutableArray *dataSourceArray; //表的数据源 组成部分:颜色数字+提示字 1为红色失败 2为绿色成功 //地图定位 CLLocationCoordinate2D myCoordinate;//定位我的位置 CLLocationManager *locationManager;//系统gps //蓝牙相关 NSTimer *signInTimer;//发起拍照命令等待计时器 NSString *peripheralName;//蓝牙名称 BOOL isConnectBlueTooth;//是否与蓝牙连接状态 NSInteger disconnectSeconds;//蓝牙断开的时长 //蓝牙每一包 NSString *blueToothOrder;//最新的蓝牙命令 用于蓝牙未回应重新作操作 BOOL dataIsLack;//如果为yes代表数据不完整 需要下次接收到的数据拼接 NSInteger lackVertifyTime;//蓝牙数据分次返回等待时间,超过5s重新发送命令 NSString *lastData;//接收蓝牙数据时 一个数据包分次返回时 之前的数据 //组合拳限制命令重复请求 不过只有照片的时候用到了。计时器1拍照启动 计时器2开始计时启动(以后不再有重要蓝牙命令) NSString *blueToothOrder_old;//记录旧的蓝牙命令,为reOrderCount服务 NSInteger reOrderCount;//蓝牙重新请求的次数 如果大于三次 不继续请求 接收到回应就重置 防治无限请求蓝牙 //图片相关 NSString *imageData;//图片数据 NSInteger imgLongth;//图片的总长度 或者每个包的长度 //升级包data NSString *nowVersion; NSString *packgeHexStr;//模拟设备升级包 //UI UIScrollView *mainScrView; NSMutableArray *detailLabArr; //学时信息表格lab UILabel *timeLabel; //计时lable NSString *logs; //训练日志 NSArray *stepTitlesArr; //步骤内容数组 //保留(未使用) UIView *trainView; //计时状态学时展示部分相关view UITableView *mainTableView; //trainview addsubview maintableview MBProgressHUD *openBlueToothMbp; } //蓝牙 @property (nonatomic, strong) CBCentralManager *cbcManager; /** 已连接的外设*/ @property (nonatomic, strong) CBPeripheral *peripheral; /** 要写入的特征值*/ @property (nonatomic, strong) CBCharacteristic *writeCharacteristic; /** 要读的特征值*/ @property (nonatomic, strong) CBCharacteristic *readCharacteristic; //当前步骤 @property(nonatomic,assign)StepImageView *currentStep; @property(nonatomic,strong)NSArray *stepsArr; @property(nonatomic,strong)NSArray *arrowArr; @property (nonatomic, strong) UIImageView *photoImg; //UI @property (strong, nonatomic) UILabel *statusLab; @property (strong, nonatomic) UIView *trainMessageView; @property (strong, nonatomic) StepImageView *step1;//设备连接 @property (strong, nonatomic) StepImageView *step2;//模拟设备升级 @property (strong, nonatomic) StepImageView *step3;//身份/设备信息验证 @property (strong, nonatomic) StepImageView *step4;//人脸比对 @property (strong, nonatomic) StepImageView *step5;//训练计时中 @property (strong, nonatomic) StepImageView *step6;//签退 @end @implementation MNQtTrainVC - (void)viewDidLoad { [super viewDidLoad]; self.title = @"模拟计时"; self.view.backgroundColor = backGroundColor; dateFormatter = [NSDateFormatter rq_defaultDateFormatter]; isConnectBlueTooth = NO; dataSourceArray = [NSMutableArray array]; logs = @""; seconds = 0; onProcessVerification = NO; processVerificationTime = [[NSString stringWithFormat:@"%@",defUser.userDict[@"mnqFacePorcess"]] integerValue]; processVerificationMaxCount = [[NSString stringWithFormat:@"%@",defUser.userDict[@"mnqFaceCount"]] integerValue]; //测试用 // processVerificationTime = 2; // processVerificationMaxCount = 2; //初始化gatherTrainDic(这一步可以拿到状态接口返回并存储的claaid等课时相关信息) [self getGatherTrainDic]; //UI [self myInit]; [self customTrainMessageView];//创建学时记录“表” [self reloadDetailLab];//教学信息更新(这个其实可以考虑放在goonTrain和beginTime) //开启定位(设置允许后台定位) [self setGPSLocationService]; //根据mn状态,下一步 [self nextWithCurrentStatu:[myDelegate.mnTrainType integerValue]]; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; } -(void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; if (signInTimer.isValid) { [signInTimer invalidate]; signInTimer = nil; } if (timer == nil) { [locationManager stopUpdatingLocation]; [self closeBlueTooth]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } #pragma mark - 设置步骤 //1-6 - (void)setCurrentStep:(StepImageView *)currentStep { if (_currentStep == currentStep) { return; } _currentStep = currentStep; NSInteger index = [_stepsArr indexOfObject:currentStep]; //箭头 if (currentStep != _step6) { UIButton *currentArrow = _arrowArr[index]; for (UIButton *arrow in _arrowArr) { if (arrow == currentArrow) { arrow.selected = YES; }else{ arrow.selected = NO; } } } //step //训练中蓝牙断开 if (_currentStep == _step1 && timer) { _step1.status = ClickStatusMid; _step2.status = ClickStatusHaveDone; _step3.status = ClickStatusHaveDone; _step4.status = ClickStatusHaveDone; _step5.status = ClickStatuseNormal; _step6.status = ClickStatusHighlighted; return; } for (StepImageView *step in _stepsArr) { if (step == _currentStep) { step.status = ClickStatusMid; if (step == _step5) { _step6.status = ClickStatusHighlighted; } break; }else{ step.status = ClickStatusHaveDone; } } } #pragma mark -添加日志记录 -(void)addLogsWithConten:(NSString *)content status:(NSInteger)status{ NSString *statusStr = @""; switch (status) { case 0: statusStr = @"失败"; break; case 1: statusStr = @"中..."; break; case 2: statusStr = @"成功"; break; default: break; } [dateFormatter setDateFormat:@"HH:mm:ss "]; NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; // NSString *logStr = [NSString stringWithFormat:@"%@: %@%@\n",dateString,content,statusStr]; // logs = [logs stringByAppendingString:logStr]; //这里和安卓的不一样 iOS日志统计的状态栏显示的东西 if (status == 4396) { logs = [logs stringByAppendingString:[NSString stringWithFormat:@"%@: %@\n",dateString,content]]; } return; } #pragma mark -UI -(void)myInit{ [self customNavigationBar]; UIScrollView *sv = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, kSize.width, kSize.height-kNavOffSet-kSafeAreaBottomHeight)]; mainScrView = sv; sv.backgroundColor = backGroundColor; [self.view addSubview:sv]; CGFloat x,y,w,h; x = y = 0; w = kSize.width; h = 56; UIView *view = [[UIView alloc]setxywh]; view.backgroundColor = [UIColor whiteColor]; [sv addSubview:view]; x = 5; h = 36; y = 10; w = 40; UILabel *lab = [[UILabel alloc]setxywh]; [lab setText:@"状态:" Font:Font17 TextColor:kTitleColor]; [view addSubview:lab]; x += w; w = kSize.width - x - 90; lab = [[UILabel alloc]setxywh]; _statusLab = lab; [lab setText:@"" Font:Font17 TextColor:subTitleColor]; [view addSubview:lab]; x = kSize.width - 75; w = 70; UIButton *checkMinRecordBtn = [UIButton buttonWithType:UIButtonTypeSystem]; checkMinRecordBtn.layer.masksToBounds = YES; checkMinRecordBtn.layer.cornerRadius = 5; checkMinRecordBtn.frame = CGRectMake(x, y, w, h); [checkMinRecordBtn setTitle:@"训练日志" textColor:[UIColor whiteColor] font:16 fotState:UIControlStateNormal]; [checkMinRecordBtn addTarget:self action:@selector(checkLogs:) forControlEvents:UIControlEventTouchUpInside]; checkMinRecordBtn.backgroundColor = defGreen; [view addSubview:checkMinRecordBtn]; x = 5; y = CGRectGetMaxY(view.frame) + 10; w = kSize.width - 10; h = w * 70/300; view = [[UIView alloc]setxywh]; _trainMessageView = view; view.backgroundColor = [UIColor whiteColor]; [sv addSubview:view]; [self customTrainMessageView]; // x = 5; y = CGRectGetMaxY(view.frame) + 10; w = kSize.width-10; h = kSize.width+50; view = [[UIView alloc]setxywh]; view.backgroundColor = [UIColor whiteColor]; [sv addSubview:view]; [self customStepsWithView:(UIView *)view]; sv.contentSize = CGSizeMake(0, y+h); } -(void)customTrainMessageView { CGFloat w = (_trainMessageView.width)/4; CGFloat h = _trainMessageView.height*0.4; CGFloat lineW = 1; NSArray *titles = @[@"所需学时",@"已完成学时",@"剩余学时",@"当天学时"]; detailLabArr = [NSMutableArray new]; for (int i=0; i<2; i++) { for (int j=0; j<4; j++) { UILabel *lab = [[UILabel alloc]initWithFrame:CGRectMake(j%4*w, h/4+i*h, w, h)]; lab.adjustsFontSizeToFitWidth = YES; lab.textAlignment = NSTextAlignmentCenter; [self.trainMessageView addSubview:lab]; if (i==0) { lab.text = titles[j]; lab.textColor = subTitleColor; }else{ lab.textColor = kTitleColor; if (j==3) { timeLabel = lab; }else{ [detailLabArr addObject:lab]; } } //line //垂直 if (j != 0) { UIView *lineVer = [[UIView alloc]initWithFrame:CGRectMake(w*j, h/2, lineW, h*1.5)]; lineVer.backgroundColor = KlineColor; [self.trainMessageView addSubview:lineVer]; } } } } - (IBAction)checkLogs:(id)sender { [PrecautionsView showPrecautionsViewWithContent:logs]; } -(void)checkPrecautions { [PrecautionsView showPrecautionsViewWithContent:@"注意:\n\n1.训练过程中,请保持蓝牙连接,蓝牙断开计时会停止。\n\n2.学员中途短暂离开无须签退;当学员再次回到模拟设备附近计时会自动恢复。若没有收到计时恢复语音提示,请前往“模拟计时”确认计时是否恢复。\n\n3.允许app切换到后台运行,允许手机黑屏,不允许关闭app。(需要设置\"始终允许访问位置\",如果选择仅在\"使用应用期间\"访问位置,则不能在后台计时。查看方式:系统设置-优易学车-位置)\n\n4.部分手机或因内存不足,在app后台运行的时候被手机系统关闭,计时停止。(如果遇到这种情况,请学员关闭其它部分app,保持“优易学车”在前台运行)\n\n5.部分地区根据规定需要进行过程验证的,请在收到过程验证通知后(app停止计时),前往模拟计时点击人脸验证,通过后将恢复计时 \n\n6.图标说明:\n浅灰色--未执行,不可操作;\n深灰色--已执行,不可操作;\n正蓝色--执行中,不可操作;\n正绿色--执行失败或未执行,可操作;\n(注:仅绿色表示可以被选中执行该步骤,其它颜色不能被选中)\n\n祝您体验愉快"]; } -(void)customStepsWithView:(UIView *)view{ NSArray *imgs = @[@"2_mn",@"1_mn",@"3_mn",@"4_mn",@"5_mn",@"6_mn"]; NSArray *titles = @[@"设备连接",@"终端升级",@"身份认证",@"人脸验证",@"训练过程",@"学员签退"]; stepTitlesArr = titles; NSArray *arrowImgs = @[@"mn_right",@"mn_down",@"mn_left",@"mn_down",@"mn_right"]; CGFloat wid = kSize.width/3; CGFloat bd = kSize.width/3/3; NSMutableArray *tmpArr = [NSMutableArray new]; NSMutableArray *jiantouArr = [NSMutableArray new]; for (int i=0; i<6; i++) { CGRect frame = CGRectMake(bd+(wid+bd)*(i%2), 0+(wid+0)*(i/2), wid, wid); if (i==2 || i==3) { frame.origin.x = bd+(wid+bd)*((i+1)%2); } //步骤 StepImageView *step = [[StepImageView alloc]initWithFrame:frame image:imgs[i] title:titles[i]]; [view addSubview:step]; [tmpArr addObject:step]; //箭头 if (i==5) { continue; } UIButton *jiantou = [UIButton buttonWithType:UIButtonTypeCustom]; jiantou.userInteractionEnabled = NO; jiantou.frame = CGRectMake(0, 0, 20, 20); if (i==1 || i==3) { jiantou.center = CGPointMake(step.center.x, CGRectGetMaxY(step.frame)+wid/8); }else{ jiantou.center = CGPointMake(kSize.width/2, step.y+step.height/2); } UIImage *normalImg = [[UIImage imageNamed:arrowImgs[i]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; [jiantou setImage:normalImg forState:UIControlStateNormal]; UIImage *sleImg = [[UIImage imageNamed:[NSString stringWithFormat:@"%@_H",arrowImgs[i]]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; [jiantou setImage:sleImg forState:UIControlStateSelected]; [view addSubview:jiantou]; [jiantouArr addObject:jiantou]; } _arrowArr = [NSArray arrayWithArray:jiantouArr]; _stepsArr = [NSArray arrayWithArray:tmpArr]; _step1 = _stepsArr[0]; _step2 = _stepsArr[1]; _step3 = _stepsArr[2]; _step4 = _stepsArr[3]; _step5 = _stepsArr[4]; _step6 = _stepsArr[5]; __weak typeof(self)weakSelf = self; _step1.clickBlock = ^{ NSLog(@"clickStep1"); weakSelf.step1.status = ClickStatusHighlighted; if([myDelegate.mnTrainType isEqualToString:@"2"]){ [weakSelf getBlueToothAndDevOrder]; }else if ([myDelegate.mnTrainType isEqualToString:@"0"]) {//正在计时 if (weakSelf.cbcManager && weakSelf.peripheral) { [weakSelf.cbcManager connectPeripheral:weakSelf.peripheral options:nil]; } } }; _step2.clickBlock = ^{ NSLog(@"clickStep2"); [weakSelf UpdateMNDev]; }; _step3.clickBlock = ^{ NSLog(@"clickStep3"); blueToothOrder = kGetDevNumAndStuCardOrder; [weakSelf sendBlueToothOrder]; }; _step4.clickBlock = ^{ NSLog(@"clickStep4"); [weakSelf takePhotoByBlueTooth]; }; _step5.clickBlock = ^{ NSLog(@"clickStep5"); }; _step6.clickBlock = ^{ NSLog(@"clickStep6"); [weakSelf wantSignOut]; }; } -(void)customNavigationBar{ //返回 UIBarButtonItem* backBbi = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"question_pre_checked_icon.png"] style:UIBarButtonItemStylePlain target:self action:@selector(goBackByNavigation)]; [backBbi setTintColor:defGreen]; [self.navigationItem setLeftBarButtonItem:backBbi]; self.navigationController.navigationBar.translucent = NO; //注意事项 backBbi = [[UIBarButtonItem alloc] initWithTitle:@"注意事项" style:UIBarButtonItemStylePlain target:self action:@selector(checkPrecautions)]; [backBbi setTintColor:defGreen]; [self.navigationItem setRightBarButtonItem:backBbi]; self.navigationController.navigationBar.translucent = NO; } -(void)goBackByNavigation{ if (self.navigationController.presentingViewController) { [self.navigationController dismissViewControllerAnimated:YES completion:nil]; }else{ [self.navigationController popViewControllerAnimated:YES]; } } #pragma mark - UI更新 -(void)reloadDetailLab{ //更新UI展示内容 if (gatherTrainDic[@"subject"]) { subject = gatherTrainDic[@"subject"]; self.navigationItem.title = [NSString stringWithFormat:@"科目%@模拟计时",[subject isEqualToString:@"2"]?@"二":@"三"]; } seconds = [gatherTrainDic[@"allTime"] integerValue]; timeLabel.text = @"00:00:00"; if (seconds != 0) { [self upDateTime]; } int i=0; for (UILabel * detailLab in detailLabArr) { switch (i) { case 0: detailLab.text = [self getTimeByMinString:gatherTrainDic[@"allNeed"]]; break; case 1: detailLab.text = [self getTimeByMinString:gatherTrainDic[@"allCompelet"]]; break; case 2: { NSInteger allComplete = [gatherTrainDic[@"allCompelet"] integerValue]; NSInteger allNeed = [gatherTrainDic[@"allNeed"] integerValue]; if (allNeed - allComplete > 0) { detailLab.text = [self getTimeByMinString:[NSString stringWithFormat:@"%ld",allNeed - allComplete]]; }else{ detailLab.text = @"0分"; } } break; default: break; } i++; } } -(NSString *)getTimeByMinString:(NSString *)minStr{ NSInteger min = [minStr integerValue]; NSInteger h = min/60; NSInteger m = min%60; if (h != 0) { return [NSString stringWithFormat:@"%ld小时%ld分",h,m]; } return [NSString stringWithFormat:@"%ld分",m]; } //更新UI上的时间 -(void)upDateTime{ int hour = (int)seconds / 3600; int min = (int)seconds % 3600 / 60; int second = (int)seconds % 3600 % 60; NSString *min_s; NSString *second_s; if (min<10) { min_s=[NSString stringWithFormat:@"0%d",min]; }else{ min_s=[NSString stringWithFormat:@"%d",min]; } if (second<10) { second_s=[NSString stringWithFormat:@"0%d",second]; }else{ second_s=[NSString stringWithFormat:@"%d",second]; } timeLabel.text = [NSString stringWithFormat:@"0%d:%@:%@",hour,min_s,second_s]; } /** 本方法会播放语音/本地通知提示,更新状态栏,新增日志记录 @param string 提示/日志/状态内容 @param audioStr 如果为nil则处于后台不添加本地通知 */ - (void)addBlueToothLabelWithContent:(NSString *)string mp3:(NSString *)audioStr{ [self addLogsWithConten:string status:4396]; _statusLab.text = string; if(audioStr){ if (myDelegate.isBackgroundTask) { [self addLocalNotificationWithAudio:audioStr]; }else { string = [string stringByReplacingOccurrencesOfString:@"重" withString:@"虫"]; [Tools playAudioWithString:string]; } } } #pragma mark - start - (void)nextWithCurrentStatu:(NSInteger )statu{ self.currentStep = _step1; /* Type 定义 0 成功获取学员信息 1 学员还未在模拟器签到(这种状态是不允许进入当前VC的) 2.已在模拟设备登录 未进行人脸比对 3 无法获取到当前训练科目 (先选科目以后,不存在该状态。科目会在签到(人脸验证)的时候提交) 4 隔日签到记录 5 分钟学时科目异常 */ //已登录但未进行人脸比对签到 if (statu == 2) { NSString *kemu = gatherTrainDic[@"subject"]; if (kemu && kemu.length != 0) { //获取蓝牙名/序列号 [self getBlueToothAndDevOrder]; }else{ [self slelectTrainSubject]; return; } return; } //成功获取学员信息 if (statu == 0) { classId = gatherTrainDic[@"classId"]; classCode = gatherTrainDic[@"classCode"]; //训练中,恢复计时 [self goonTrain]; return; } //隔日训练 if (statu == 4) { _step6.status = ClickStatusMid; //查看是否存在本地学时 minuteTrainArray = [DB_Helper quearyTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; if (minuteTrainArray.count > 0) { [self uploadAppTrainPlWithSignOut:YES]; }else{ [gatherTrainDic setValue:@"2" forKey:@"isOver"]; classId = gatherTrainDic[@"classId"]; classCode = gatherTrainDic[@"classCode"]; minuteTrainArray = [DB_Helper quearyTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; if (minuteTrainArray.count > 0) { [self uploadAppTrainPlWithSignOut:YES]; }else{ [self signOut]; } } return; } //以下两种状态(1,3)此处无需判断 //学员还未在模拟器签到(这种状态是不允许进入当前VC的,可以无视) // if(statu == 1){ // // UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:@"提醒" message:@"当前暂未检测到PC端已登录信息;如需计时,请扫描PC端登录二维码完成登录" preferredStyle:UIAlertControllerStyleAlert]; // // [alertFind addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { // [self goBackByNavigation]; // }]]; // [alertFind addAction:[UIAlertAction actionWithTitle:@"扫码登录" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { // // [self openScan]; // }]]; // [alertFind addAction:[UIAlertAction actionWithTitle:@"PC端已登录" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { // // [self getCurrentState];打开的时候考虑一下获取状态后的操作 // }]]; // // [self presentViewController:alertFind animated:true completion:nil]; // // return; // } if (statu == 5) { UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"分钟学时科目异常" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [self goBackByNavigation]; }]]; [self presentViewController:alertFind animated:true completion:nil]; return; } if (statu == 3) { UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"无法获取到当前训练科目" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [self goBackByNavigation]; }]]; [self presentViewController:alertFind animated:true completion:nil]; return; } } #pragma mark 蓝牙相关 - (void)searchBlueTooth { dataIsLack = NO; lastData = @""; blueToothOrder = @""; reOrderCount = 0; if ([myDelegate.mnTrainType isEqualToString: @"0"]) {//说明是断线重连 minuteTrainDic = [NSMutableDictionary dictionary]; [minuteTrainDic setValue:gatherTrainDic[@"classId"] forKey:@"classId"]; [minuteTrainDic setValue:gatherTrainDic[@"classCode"] forKey:@"classCode"]; [minuteTrainDic setValue:gatherTrainDic[@"trainNum"] forKey:@"trainNum"]; trainNum = gatherTrainDic[@"trainNum"]; photoSeconds = [gatherTrainDic[@"photoTime"] integerValue]; timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];//处理UI交互阻塞计时 myDelegate.mnPeriodVC = self; // [self addBlueToothLabelWithContent:@"断线重连成功,已恢复训练" mp3:nil]; // [Tools playAudioWithString:@"断线虫连成功,已恢复训练"]; disconnectSeconds = 4;//替换上面的提醒zzz } if (!_cbcManager) { _cbcManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; } } //只要中心管理者初始化 就会触发此代理方法 判断手机蓝牙状态 - (void)centralManagerDidUpdateState:(CBCentralManager *)central { switch (central.state) { case 0: //状态未知 //NSLog(@"CBCentralManagerStateUnknown"); break; case 1: //连接断开 即将重置 //NSLog(@"CBCentralManagerStateResetting"); break; case 2: //该平台不支持蓝牙 //NSLog(@"CBCentralManagerStateUnsupported"); break; case 3: //未授权蓝牙使用 hovertree.com //NSLog(@"CBCentralManagerStateUnauthorized"); { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"温馨提示" message:@"请在“设置”中打开蓝牙访问权限" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alert show]; } break; case 4: { //蓝牙未开启 [self addBlueToothLabelWithContent:@"蓝牙未开启" mp3:nil]; //NSLog(@"CBCentralManagerStatePoweredOff"); if (peripheralName.length > 0) { // showMsgByAlert(self, @"请打开手机蓝牙"); openBlueToothMbp = [MBProgressHUD showMessage:@"请打开手机蓝牙" toView:self.view];//这个加载每一次都是新建的 不用担心和其它公用 [Tools playAudioWithString:@"请打开手机蓝牙"]; } self.currentStep = _step1; isConnectBlueTooth = NO; } break; case 5: { //蓝牙已开启 if (openBlueToothMbp) { [openBlueToothMbp hideAnimated:YES]; } [self addBlueToothLabelWithContent:@"蓝牙已开启" mp3:nil]; //NSLog(@"CBCentralManagerStatePoweredOn"); if (peripheralName.length < 1) { return; } // 搜索外设 if (self.peripheral) { return; } // 搜索成功之后,会调用找到外设的代理方法 [self addBlueToothLabelWithContent:@"正在连接模拟设备..." mp3:nil]; [self.cbcManager scanForPeripheralsWithServices:nil // 通过某些服务筛选外设 options:nil]; // dict,条件 } break; default: break; } } // 发现外设后调用的方法 - (void)centralManager:(CBCentralManager *)central // 中心管理者 didDiscoverPeripheral:(CBPeripheral *)peripheral // 外设 advertisementData:(NSDictionary *)advertisementData // 外设携带的数据 RSSI:(NSNumber *)RSSI // 外设发出的蓝牙信号强度 { NSLog(@"搜索到的外设----><>%@----><>%@",peripheral.name,peripheralName); //如果蓝牙名称和二维码中的一致 就可以连接了 最好能关闭蓝牙的扫描 if ([peripheral.name containsString:peripheralName]) { self.peripheral = peripheral; [self.cbcManager stopScan]; [_cbcManager connectPeripheral:self.peripheral options:nil]; } } // 中心管理者连接外设成功 - (void)centralManager:(CBCentralManager *)central // 中心管理者 didConnectPeripheral:(CBPeripheral *)peripheral // 外设 { NSLog(@"%s, line = %d, %@=连接成功", __FUNCTION__, __LINE__, peripheral.name); isConnectBlueTooth = YES; // 连接成功之后,可以进行服务和特征的发现 // 设置外设的代理 self.peripheral.delegate = self; // 外设发现服务,传nil代表不过滤 [self.peripheral discoverServices:@[ [CBUUID UUIDWithString:kServiceUUID] ]]; if (!timer) {//非计时 [self addBlueToothLabelWithContent:@"模拟设备连接成功" mp3:nil];//如果学员在150s内没有开始计时启动timer 那么训练日志里面就会多出一条这个记录 }else{//计时 //计时离线超过3s语音提示/恢复按钮UI步骤。timer存在且大于3 计时中 if (disconnectSeconds > 3){ //判断是否需要执行过程验证,恢复到不同的操作 if (onProcessVerification) { [self addBlueToothLabelWithContent:@"模拟设备连接成功" mp3:nil]; _currentStep = _step4;//不触发setCurrentStep 步骤的方法 _step4.status = ClickStatusHighlighted;//这里不考虑学员自己在照片传输到一半的过程中,离开蓝牙范围。统一激活人脸签到按钮,点击可以重新发拍照指令。 _step1.status = ClickStatusHaveDone; _step5.status = ClickStatusHaveDone; }else{ self.currentStep = _step5; //这里恢复计时的时候判断一下是否需要过程验证 if (photoSeconds%(processVerificationTime*60) != 0) { [self addBlueToothLabelWithContent:@"设备连接成功,恢复计时" mp3:@"mn_blueToothConnect.mp3"]; } } } } disconnectSeconds = 0; } // 外设连接失败 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { isConnectBlueTooth = NO; NSLog(@"%s, line = %d, %@=连接失败", __FUNCTION__, __LINE__, peripheral.name); [self addBlueToothLabelWithContent:@"模拟设备连接失败!" mp3:nil]; if (self.peripheral) { [_cbcManager connectPeripheral:self.peripheral options:nil]; } } // 丢失连接 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@"%s, line = %d, %@=断开连接", __FUNCTION__, __LINE__, peripheral.name); isConnectBlueTooth = NO; if (self.cbcManager && self.peripheral) { [self.cbcManager connectPeripheral:self.peripheral options:nil]; } //因为硬件那边的某些问题 现在设置连接间隔不符合苹果的要求 会固定每155s(测试)断开一次。所以断开提示设置为3s之后才提示 dispatch_time_t zz = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC); dispatch_after(zz, dispatch_get_main_queue(), ^{ if (timer && !isConnectBlueTooth) { self.currentStep = _step1; _step1.status = ClickStatusHighlighted; if (timer) { [self addBlueToothLabelWithContent:@"设备连接断开,计时停止" mp3:@"mn_blueToothDisconnect.mp3"]; }else{ [self addBlueToothLabelWithContent:@"设备连接断开" mp3:nil]; } } }); } //查找服务的所有特征 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { if (error) { NSLog(@"Discovered services for %@ with error: %@", peripheral.name, [error localizedDescription]); [self addBlueToothLabelWithContent:@"蓝牙服务搜索失败" mp3:nil]; return; } //NSLog(@"services---><>%@",peripheral.services); //服务并不是我们的目标,也没有实际意义。我们需要用的是服务下的特征,查询(每一个服务下的若干)特征 for (CBService *service in peripheral.services) { NSLog(@"Service found with UUID: %@", service.UUID); // Discovers the characteristics for a given service if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) { //搜索特性 [self.peripheral discoverCharacteristics:nil forService:service]; } } } // 发现外设服务里的特征的时候调用的代理方法(这个是比较重要的方法,你在这里可以通过事先知道UUID找到你需要的特征,订阅特征,或者这里写入数据给特征也可以) - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { if (error) { NSLog(@"搜索特性失败 characteristic: %@", [error localizedDescription]); [self addBlueToothLabelWithContent:@"蓝牙服务特征搜索失败" mp3:nil]; return; } for (CBCharacteristic *cha in service.characteristics) { if ([cha.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID_reade]]) { // NSLog(@"读取特征-->>>%@",cha); self.readCharacteristic = cha; [peripheral setNotifyValue:YES forCharacteristic:cha]; /* 最后还有一个重要的补充,当我们已经找到了我们感兴趣的characteristic后,他的value并不一定通过1方法或者2方法就可以接收的,就算可以接收,有时候只能用1,有时候只能用2。 这牵扯到characteristic的一个property 当为read(0x02)的时候,我们用1方法可以查看,用2就会出错。当为notify(0x10)的时候我们就得用2方法。其他就不说明了,比如write就只能写value······ typedef enum { CBCharacteristicPropertyBroadcast = 0x01, CBCharacteristicPropertyRead = 0x02, CBCharacteristicPropertyWriteWithoutResponse = 0x04, CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired = 0x200, } CBCharacteristicProperties; 所以当我们想查看value的时候,先了解一下这个characteristic的property,看是不是能让我们读的。怎么查看?找BLE外设的设备厂商或者查看外设的说明书。 推荐将value经常变化的characteristic的property设为notify */ } if ([cha.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID_write]]) { // NSLog(@"写入特征-->>>%@",cha);//0x04对应的是CBCharacteristicPropertyWriteWithoutResponse || 0x0A对应的是CBCharacteristicPropertyNotify self.writeCharacteristic = cha; bool isReal = [myDelegate.mnTrainType integerValue] == 2;//走到这一步只会有 0 成功获取学员信息(学员是计时状态 != app在跑定时器) / 2.已在模拟设备登录,未进行人脸比对 if (isReal && (_currentStep == _step1 || _currentStep == _step2)) {//未开始 //获取版本号 self.currentStep = _step2; blueToothOrder = kGetMNdevVersion; [self sendBlueToothOrder]; } /*非计时状态,中间断开,重发蓝牙指令? 1.现在每一步都可以点击按钮重新操作,即学员主动发送蓝牙指令; 2.终端重新连接后会继续以前的动作。app会响应收到的数据执行相关操作 鉴于以上两点,app就不主动重新发送蓝牙连接了*/ //if(blueToothOrder.length > 0){ // [self sendBlueToothOrder]; //} } } } - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error { if (error) { NSLog(@"Error changing notification state: %@", error.localizedDescription); [self addBlueToothLabelWithContent:@"蓝牙服务特征状态异常" mp3:nil]; return; } // Exits if it's not the transfer characteristic if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID_reade]]) { return; } // Notification has started if (characteristic.isNotifying) { NSLog(@"Notification began on %@", characteristic); NSLog(@"%@",characteristic.value); NSLog(@"%@",[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]); // [peripheral readValueForCharacteristic:characteristic]; 这里不要再去读啊,否则会收到每一次发送数据的最后一包(20) } else { // Notification has stopped NSLog(@"Notification stopped on %@. Disconnecting", characteristic); } } // 更新特征的value的时候会调用 (凡是从蓝牙传过来的数据都要经过这个回调,简单的说这个方法就是你拿数据的唯一方法) - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if ([characteristic isEqual:self.readCharacteristic] ) { NSString *hexString = [Tools convertDataToHexStr:characteristic.value]; NSLog(@"hexString--->%ld<>%@",seconds,hexString); lackVertifyTime = 0; //数据完整性检测 if ([hexString containsString:@"be"] && dataIsLack == NO) { //数据首包 lastData = hexString; if (lastData.length < 6) { return; } int dataLength = [Tools getHexNumWithString:[lastData substringWithRange:NSMakeRange(2, 4)]]; if (lastData.length < dataLength*2 + 6) { dataIsLack = YES; return; } }else { if (dataIsLack) {//上个包数据不够 hexString = [lastData stringByAppendingString:hexString]; lastData = hexString; int dataLength = [Tools getHexNumWithString:[lastData substringWithRange:NSMakeRange(2, 4)]]; if (lastData.length < dataLength*2 + 6) { return; }else { dataIsLack = NO; } }else{ return; } } if ([hexString containsString:@"be"]) {//防止其它莫名奇妙的指令 NSString *order = [hexString substringWithRange:NSMakeRange(6, 2)]; //获取到模拟设备版本,进行版本判断 if([order isEqualToString:@"0a"]){ [self checkDevVersionWithHexString:hexString]; return; } //升级 if ([order isEqualToString:@"0b"]) { [self sendMsgWithSubPackageWithHexString:hexString];//发送指定升级包 return; } //升级成功 if ([order isEqualToString:@"0c"]) { ShowMsg(@"模拟设备升级完成"); [self addBlueToothLabelWithContent:@"模拟设备升级完成" mp3:nil]; [Tools playAudioWithString:@"模拟设备升级完成"]; return; } //获取到终端信息 if ([order isEqualToString:@"03"]) { [self signFirstCheckWithHexString:hexString];//验证序列号/物理卡id return; } //拍照成功开始返回的照片包(第一包开始) if ([order isEqualToString:@"17"]) { [self getDataForPictureWithString:hexString]; return; } } } } -(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error{ if (error) { NSLog(@"写入数据失败-->%@:%@",blueToothOrder,error); }else{ NSLog(@"写入数据成功-->: %@",blueToothOrder); } } //根据数据内容生成图片 - (void)getDataForPictureWithString:(NSString *)hexString { //取相片 NSString *len = [hexString substringWithRange:NSMakeRange(2, 4)]; NSInteger packageLength = [Tools getHexNumWithString:len]; if ([blueToothOrder containsString:kGetPhotoData]) { NSString *newData = [[hexString substringFromIndex:10] stringByReplacingOccurrencesOfString:@"eded" withString:@""]; imageData = [imageData stringByAppendingString:newData];//每次发起拍照命令前 将图片数据清空 NSString *currentHexOrder = [hexString substringWithRange:NSMakeRange(8, 2)]; int pOrderNum = [Tools getHexNumWithString:currentHexOrder]; if ([currentHexOrder isEqualToString:@"00"]) {//循环发送命令 //照片数据全取出 显示照片 NSData *image = [Tools convertHexStrToData:imageData]; //获取到照片 上传服务器 if (image) { blueToothOrder = @"upLoadWAIT"; //如果是签到拍照生成classId等相关 if (onProcessVerification == NO) { [self getClassIdAndSoOn]; } [self upLoadPhotoWithImage:[UIImage imageWithData:image]]; } }else{ NSString *orderStr = [NSString stringWithFormat:@"%.2x",pOrderNum + 1];////命令+1 然后转为16进制 blueToothOrder = [NSString stringWithFormat:@"%@%@eded",kGetPhotoData,orderStr]; [self sendBlueToothOrder]; } return; } //如果是拍照指令 并且 返回了第一包的数据 if ([blueToothOrder isEqualToString:kTakePhotoOrder] && [[hexString substringWithRange:NSMakeRange(6, 2)] isEqualToString:@"17"]) { [self addBlueToothLabelWithContent:@"拍照已完成,正在采集照片信息..." mp3:@"mn_takePhotoColloctPic.mp3"]; //拍照成功 发指令获取数据 if (packageLength*2 == hexString.length-6 && [hexString containsString:@"eded"]) {//判断完整性 imageData = [[hexString substringFromIndex:10] stringByReplacingOccurrencesOfString:@"eded" withString:@""];//每次发起拍照命令前 将图片数据清空 blueToothOrder = [NSString stringWithFormat:@"%@02eded",kGetPhotoData];//获取指定图像包 }else{ blueToothOrder = [NSString stringWithFormat:@"%@01eded",kGetPhotoData];//获取指定图像包 } [self sendBlueToothOrder]; } } #pragma mark 一些方法 -(void)checkDevVersionWithHexString:(NSString *)hexString{ NSMutableString *mStr = [NSMutableString stringWithString:hexString]; NSString *str = [[mStr substringFromIndex:6] stringByReplacingOccurrencesOfString:@"eded" withString:@""]; NSArray *arr = [[Tools convertHexStrToString:str] componentsSeparatedByString:@";"]; NSInteger zz = [[NSString stringWithFormat:@"%@",arr[1]] integerValue]; nowVersion = [NSString stringWithFormat:@"%ld",zz]; //版本更新检查 [self UpdateMNDev];//这返回了三个参数,DM91-MN,0003,L000003 我们只用到中间的序列号 } - (void)signFirstCheckWithHexString:(NSString *)hexString{ NSMutableString *mStr = [NSMutableString stringWithString:hexString]; NSString *str = [[mStr substringFromIndex:6] stringByReplacingOccurrencesOfString:@"eded" withString:@""]; NSArray *arr = [[Tools convertHexStrToString:str] componentsSeparatedByString:@";"]; //如果是返回序列号,判断是否符合服务器返回的序列号,符合则命令拍照 if ([arr[1] isEqualToString: mnqDev.mnqSn]) { if ([arr[0] containsString:mnqDev.cardId]) { [self addBlueToothLabelWithContent:@"身份信息认证成功" mp3:nil]; self.currentStep = _step4; //发起拍照命令 [self takePhotoByBlueTooth]; }else{ _currentStep.status = ClickStatusHighlighted; showMsgByAlertWithSureBlock(self, @"请刷本人身份证", ^{ _currentStep.status = ClickStatusMid; blueToothOrder = kGetDevNumAndStuCardOrder; [self sendBlueToothOrder]; }); [Tools playAudioWithString:@"请刷本人身份证"]; [self addBlueToothLabelWithContent:@"身份信息认证失败" mp3:nil]; } }else{ _currentStep.status = ClickStatusHighlighted; showMsgByAlert(self, @"模拟设备序列号不匹配"); [Tools playAudioWithString:@"模拟设备序列号不匹配"]; [self addBlueToothLabelWithContent:@"身份信息认证失败" mp3:nil]; } } - (void)showAlertTakePhotoFailWithStr:(NSString *)failureStr { _currentStep.status = ClickStatusHighlighted; blueToothOrder = @""; UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:[NSString stringWithFormat:@"人脸比对失败(%@),是否重新采集照片",failureStr] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { //关闭定时器 if (signInTimer) { [signInTimer invalidate]; signInTimer = nil; } }]]; [alert addAction:[UIAlertAction actionWithTitle:@"重新拍照" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { _currentStep.status = ClickStatusMid; [self performSelector:@selector(takePhotoByBlueTooth) withObject:nil afterDelay:1]; }]]; [self presentViewController:alert animated:NO completion:nil]; } - (void)wantSignOut{ UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"您确定要结束当前模拟计时训练吗?" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { _step6.status = ClickStatusHighlighted; }]]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { self.currentStep = _step6; //先关闭定时器 if (timer.isValid) { [timer invalidate]; timer = nil; myDelegate.mnPeriodVC = nil; } //关闭蓝牙连接 [self closeBlueTooth]; [gatherTrainDic setValue:@"2" forKey:@"isOver"]; [self saveGatherTrainDic]; minuteTrainArray = [DB_Helper quearyTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; if (minuteTrainArray.count > 0) { [self uploadAppTrainPlWithSignOut:YES]; }else{ [self signOut]; } }]]; [self presentViewController:alertFind animated:true completion:nil]; } - (void)removeImage { if (_photoImg) { [_photoImg removeFromSuperview]; } } #pragma mark - 蓝牙写入数据 - (void)sendBlueToothOrder{ //不同蓝牙指令重置失败次数 if (blueToothOrder != blueToothOrder_old) { blueToothOrder_old = blueToothOrder; reOrderCount = 0; } lackVertifyTime = 0; dataIsLack = NO; lastData = nil; if (blueToothOrder && blueToothOrder.length != 0) { [self.peripheral writeValue:[Tools convertHexStrToData:blueToothOrder] forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithResponse]; } } #pragma mark - 拍照命令 //发送拍照命令 - (void)takePhotoByBlueTooth { imageData = @""; lastData = @""; imgLongth = 0; reOrderCount = 0; //发送拍照指令 [Tools playAudioWithString:@"拍照准备"]; dispatch_time_t zz = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC); dispatch_after(zz, dispatch_get_main_queue(), ^{ blueToothOrder = kTakePhotoOrder; [self sendBlueToothOrder];//每次发送蓝牙指令lackVertifyTime = 0; }); if (!signInTimer) { signInTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(signInTimerRun) userInfo:nil repeats:YES]; } } //此定时器仅存在于拍照的整个过程 - (void)signInTimerRun { if (isConnectBlueTooth) { //每一包分批次等待时间 lackVertifyTime++; if (dataIsLack && lackVertifyTime == 5) { reOrderCount ++; if (reOrderCount < 3) { [self sendBlueToothOrder];//重发拍照/获取图片相关指令 }else { //apap蓝牙指令交流失败 showMsgByAlert(self, @"拍照失败!"); [Tools playAudioWithString:@"拍照失败!"]; _currentStep.status = ClickStatusHighlighted; } } } } - (void)closeBlueTooth { if (peripheralName.length > 0) { //断开连接 if (self.peripheral) { [self.cbcManager cancelPeripheralConnection:self.peripheral]; } self.peripheral.delegate = nil; self.peripheral = nil; self.writeCharacteristic = nil; self.readCharacteristic = nil; self.cbcManager = nil; } } #pragma mark - 模拟设备升级 - (void)startUpdateMNQDevWithFilePath:(NSURL *)filePath{ MBProgressHUD *hud = [MBProgressHUD HUDForView:self.view]; hud.label.text = @"模拟设备升级中"; hud.mode = MBProgressHUDModeDeterminateHorizontalBar; NSData *data = [NSData dataWithContentsOfURL:filePath]; packgeHexStr = [Tools convertDataToHexStr:data]; [self sendMsgWithSubPackageWithHexString:@"be00040b01eded"];//这里模仿外设请求第一包 } -(void)sendMsgWithSubPackageWithHexString:(NSString *)hexStr{ NSString *orderStr = [hexStr substringWithRange:NSMakeRange(8, 2)]; NSInteger order = [Tools getHexNumWithString:orderStr]; NSString *perHexString = nil; MBProgressHUD *hud = [MBProgressHUD HUDForView:self.view]; if (packgeHexStr.length == 0) { //升级完成 perHexString = [NSString stringWithFormat:@"be00048b00eded"]; hud.progress = 1.0; dispatch_async(dispatch_get_main_queue(), ^{ [MBProgressHUD hideHUDForView:self.view]; }); }else if (packgeHexStr.length > order*1792) { perHexString = [NSString stringWithFormat:@"be%.4x8b%@%@eded",BLE_SEND_PER_LEN,orderStr,[packgeHexStr substringWithRange:NSMakeRange(order*1792-1792, 1792)]]; hud.progress = 1.0*order/(packgeHexStr.length/1792); }else{ NSString *newStr = [packgeHexStr substringWithRange:NSMakeRange(order*1792-1792, packgeHexStr.length%1792)]; perHexString = [NSString stringWithFormat:@"be%.4lx8b%@%@eded",packgeHexStr.length%1792/2+4,orderStr,newStr]; packgeHexStr = @""; } NSLog(@"升级包==> : %ld == %@ ",perHexString.length,perHexString); [self sendMsgWithSubPackage:[Tools convertHexStrToData:perHexString]]; } //分包发送蓝牙数据 -(void)sendMsgWithSubPackage:(NSData*)msgData { for (int i = 0; i < [msgData length]; i += BLE_SEND_MAX_LEN) { // 预加 最大包长度,如果依然小于总数据长度,可以取最大包数据大小 if ((i + BLE_SEND_MAX_LEN) < [msgData length]) { NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, BLE_SEND_MAX_LEN]; NSData *subData = [msgData subdataWithRange:NSRangeFromString(rangeStr)]; NSLog(@"升级小小包subData:%@",subData); [self.peripheral writeValue:subData forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithoutResponse];// 这里之所以用这个属性是为了提升发送速度。用带回应的也能写入成功 只是因为回调结果导致过程太慢 //根据接收模块的处理能力做相应延时 usleep(20 * 1000); } else { NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, (int)([msgData length] - i)]; NSData *subData = [msgData subdataWithRange:NSRangeFromString(rangeStr)]; NSLog(@"升级小小包subData:%@",subData); [self.peripheral writeValue:subData forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithoutResponse]; usleep(20 * 1000); } } } #pragma mark - 添加本地通知 -(void)addLocalNotificationWithAudio:(NSString *)audioName{ //定义本地通知对象 UILocalNotification *notification = [[UILocalNotification alloc]init]; //设置调用时间 notification.fireDate = [NSDate date]; //设置通知属性 NSString *bodyString = @"优易学车提醒您"; if ([audioName isEqualToString:@"mn_takePhotoColloctPic.mp3"]) { bodyString = @"拍照已完成,正在采集照片数据";//其实这个mp3 感觉也是没太必要的。这个时候都没计时 学员应该多关注一些手机,app在前台才对 }else if ([audioName isEqualToString:@"mn_blueToothConnect.mp3"]) { bodyString = @"模拟设备连接成功"; }else if ([audioName isEqualToString:@"mn_blueToothDisconnect.mp3"]) { bodyString = @"模拟设备已断开,计时暂停,正在尝试重新连接"; }else if ([audioName isEqualToString:@"mn_blueToothQuit.mp3"]) { bodyString = @"模拟计时已签退,本次计时结束"; }else if ([audioName isEqualToString:@"mn_processVerification.mp3"]){ bodyString = @"计时停止,请进行拍照验证"; } notification.alertBody = bodyString; //通知主体 notification.applicationIconBadgeNumber = 1;//应用程序图标右上角显示的消息数 notification.alertAction = @"打开应用"; //待机界面的滑动动作提示 notification.soundName = audioName;//收到通知时播放的声音,默认消息声音 NSDictionary *userDict = [NSDictionary dictionaryWithObjectsAndKeys:@"nothing",@"MNQTrainVC", nil]; notification.userInfo = userDict; //调用通知 [[UIApplication sharedApplication] scheduleLocalNotification:notification]; } #pragma mark gps定位相关 -(void)setGPSLocationService{ //实例化manager locationManager=[[CLLocationManager alloc]init]; //设置代理 locationManager.delegate=self; //设置定位精度 //定位要求的精度越高、属性distanceFilter的值越小,应用程序的耗电量就越大。 locationManager.desiredAccuracy=kCLLocationAccuracyNearestTenMeters; //定位距离 locationManager.distanceFilter=50; //这里定位只是为了使其后台运行 //申请定位许可,iOS8以后特有 /** 由于IOS8中定位的授权机制改变 需要进行手动授权 * 获取授权认证,两个方法: * [self.locationManager requestWhenInUseAuthorization]; * [self.locationManager requestAlwaysAuthorization]; */ if([locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) { [locationManager requestAlwaysAuthorization]; } if ([[UIDevice currentDevice].systemVersion floatValue] > 9) { /** iOS9新特性:将允许出现这种场景:同一app中多个location manager:一些只能在前台定位,另一些可在后台定位(并可随时禁止其后台定位)。 */ [locationManager setAllowsBackgroundLocationUpdates:YES]; } //开始定位 [locationManager startUpdatingLocation]; } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{ /* kCLErrorDenied,//访问位置或范围已被用户拒绝 kCLErrorNetwork,//一般,与网络相关的错误 */ NSLog(@"位置访问失败"); UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@"温馨提示" message:@"请在iPhone的“设置”-“隐私”-“定位服务”功能中,找到“优易学车”打开位置访问权限" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil]; [alert show]; [locationManager stopUpdatingLocation]; } //- (void)locationManager:(CLLocationManager *)manager // didUpdateToLocation:(CLLocation *)newLocation // fromLocation:(CLLocation *)oldLocation //{ // //打印出经度和纬度 // CLLocationCoordinate2D coordinate = newLocation.coordinate; // NSDictionary *testdic =BMKConvertBaiduCoorFrom(coordinate,BMK_COORDTYPE_GPS);//转换GPS坐标至百度坐标(加密后的坐标) // //解密加密后的坐标字典 // CLLocationCoordinate2D baiduCoor = BMKCoorDictionaryDecode(testdic);//转换后的百度坐标 // myCoordinate = baiduCoor; //} - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(nonnull NSArray *)locations { CLLocation *loctaion = [locations firstObject]; CLLocationCoordinate2D coordinate = loctaion.coordinate; myCoordinate = BMKCoordTrans(coordinate, BMK_COORDTYPE_GPS, BMK_COORDTYPE_BD09LL);//转换GPS坐标至百度坐标(加密后的坐标) } //#pragma mark 打开相机扫描二维码 //-(void)openScan{ // ScanVC *scan = [[ScanVC alloc] init]; // scan.type = @""; // [scan scanBlock:^(NSString *dataString) { // if ([dataString isEqualToString:@"success"]) { // myDelegate.mnTrainType = @"2"; // [self getBlueToothAndDevOrder]; // } // }]; // [self navPushHideTabbarToVC:scan]; //} #pragma mark - 本地数据读取 - (void)saveGatherTrainDic { NSString *filePath = [Tools getPathWithFileName:@"MNQTrainDic.plist"]; [gatherTrainDic writeToFile:filePath atomically:YES]; } - (void)getGatherTrainDic { NSString *filePath = [Tools getPathWithFileName:@"MNQTrainDic.plist"]; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { gatherTrainDic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; } } #pragma mark tableview -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return dataSourceArray.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; cell.selectionStyle = UITableViewCellSelectionStyleNone; } [cell.textLabel setText:[dataSourceArray[dataSourceArray.count - indexPath.row - 1] substringFromIndex:1] Font:Font17 TextColor:[[dataSourceArray[dataSourceArray.count - indexPath.row - 1] substringToIndex:1] isEqualToString:@"1"]?[UIColor redColor]:[UIColor blackColor]]; [cell.textLabel setNumberOfLines:0]; return cell; } #pragma mark -选择科目 - (void)slelectTrainSubject{ BOOL isOpenTwo = [defUser.userDict[@"mnqTwoOpen"] isEqualToString:@"1"]; BOOL isOpenThree = [defUser.userDict[@"mnqThreeOpen"] isEqualToString:@"1"]; //同时开启 if (isOpenTwo && isOpenThree) { UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"请选择训练科目" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"科二" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { subject = @"2"; self.navigationItem.title = [NSString stringWithFormat:@"科目%@模拟计时",[subject isEqualToString:@"2"]?@"二":@"三"]; [self getBlueToothAndDevOrder]; }]]; [alertFind addAction:[UIAlertAction actionWithTitle:@"科三" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { subject = @"3"; self.navigationItem.title = [NSString stringWithFormat:@"科目%@模拟计时",[subject isEqualToString:@"2"]?@"二":@"三"]; [self getBlueToothAndDevOrder]; }]]; [self presentViewController:alertFind animated:true completion:nil]; }else if (isOpenTwo || isOpenThree){ //其中一个开启 subject = isOpenTwo ? @"2" : @"3"; self.navigationItem.title = [NSString stringWithFormat:@"科目%@模拟计时",[subject isEqualToString:@"2"]?@"二":@"三"]; [self getBlueToothAndDevOrder]; }else{ //都未开启 UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"暂未开启模拟计时" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [self goBackByNavigation]; }]]; [self presentViewController:alertFind animated:true completion:nil]; } } #pragma mark - 继续计时 - (void)goonTrain{ //蓝牙 peripheralName = gatherTrainDic[@"blueName"]; if (peripheralName.length > 0) { //如果之前有蓝牙 现在也要重新开启起来 [self searchBlueTooth]; }else{ [self getBlueToothAndDevOrder]; return; } } #pragma mark 开始计时 - (void)beginTimer { //更新表统计 [self reloadDetailLab]; //过程验证计时 photoSeconds = 0; self.currentStep = _step5; //过程中不需要再发送重要指令。该定时器主要处理拍照命令 if (signInTimer) { [signInTimer invalidate]; signInTimer = nil; } myDelegate.mnTrainType = @"0"; [self addBlueToothLabelWithContent:@"签到成功,开始计时" mp3:nil]; [Tools playAudioWithString:[NSString stringWithFormat:@"开始训练,训练过程中允许手机黑屏,请不要关闭应用,否则会影响学时上传,祝您体验愉快"]]; //将之前的学时全部清空 [DB_Helper deleteAllTrainWithType:mnTableName]; [gatherTrainDic setValue:subject forKey:@"subject"];//训练科目 [gatherTrainDic setValue:peripheralName forKey:@"blueName"];//蓝牙名 [gatherTrainDic setValue:classId forKey:@"classId"];//课堂id [gatherTrainDic setValue:classCode forKey:@"classCode"];//课程编码 [gatherTrainDic setValue:trainNum forKey:@"trainNum"];//学时编号 [gatherTrainDic setValue:@"0" forKey:@"photoSeconds"];//秒 服务器训练状态返回接口(登录完之后调用)/以及人脸比对接口会返回更新该值 [gatherTrainDic setValue:@"0" forKey:@"isOver"];//是否结束 [self saveGatherTrainDic];//保存至本地 可以用于断线重连 //classId , classCode , trainNum minuteTrainDic = [NSMutableDictionary dictionary]; [minuteTrainDic setValue:classId forKey:@"classId"]; [minuteTrainDic setValue:classCode forKey:@"classCode"]; [minuteTrainDic setValue:trainNum forKey:@"trainNum"]; disconnectSeconds = 0; timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];//处理UI交互阻塞计时 trainView.hidden = NO; myDelegate.mnPeriodVC = self; } - (void)timerRun { NSLog(@"timerRuning"); //计时过程中蓝牙断开连接 if (!isConnectBlueTooth && peripheralName.length > 0) { disconnectSeconds ++; //如果超过10分钟 停止每5s自动连接 if (disconnectSeconds >= 10*60) { return; } if (disconnectSeconds%5 == 0) { if (_cbcManager && self.peripheral) { //[Tools playAudioWithString:@"设备连接中,请稍等"];这个在中途断开的时候 还是不要提示了 [_cbcManager connectPeripheral:self.peripheral options:nil]; } } return; } //每processVerificationTime进行过程验证 photoSeconds!= 0 第一次进来的时候规避掉过程验证 if (photoSeconds !=0 && processVerificationTime != 0 && onProcessVerification == NO) {//processVerificationTime不等于0表示需要验证 if (photoSeconds%(processVerificationTime*60) == 0) { onProcessVerification = YES; processVerificationFailureCount = 0; /*是否停止计时器 为了防止学员在收到提醒以后并且长时间(>3s)离开了蓝牙连接范围,为了走前面的每5s自动连接蓝牙重连操作,所以还是不停止计时器了 */ _currentStep = _step4;//不触发setCurrentStep 步骤的方法 _step5.status = ClickStatusHaveDone; #if 0 //手机通知学员 [self addBlueToothLabelWithContent:@"计时停止,请进行拍照验证" mp3:@"mn_processVerification.mp3"]; //让学员发起过程验证拍照指令 _step4.status = ClickStatusHighlighted; UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"过程验证" message:@"模拟计时已停止,请前往 个人-模拟计时 进行人脸验证" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil]; [servicesDisabledAlert show]; #else //手机不通知学员,添加日志记录 NSString *string = @"计时停止,请进行拍照验证"; [self addLogsWithConten:string status:4396]; _statusLab.text = string; //这种直接发起拍照命令的方式,符合过程验证“抓拍”的意义。但是鉴于只有app向学员传达了训练状态这一点,还是让学员主动操作手机进行过程验证吧。如果需要改的话 就用下面这种方式 /*5s是为了等待前面的语音提示完成,避免与拍照语音提示冲突; 这里是否要考虑如果是过程验证拍照,想要在手机黑屏的情况下让学员完成验证过程,怎么做比较好呢?该怎么和学员交流呢 */ _step4.status = ClickStatusMid; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self takePhotoByBlueTooth]; }); #endif } } //过程验证,停止计时 if (onProcessVerification) { return; } //计时器正常运作(蓝牙一定是连接的,且不处于过程验证状态) seconds++; photoSeconds++; NSLog(@"%ld---->最新蓝牙指令:%@ ---->拍照时间:%ld",seconds,blueToothOrder,photoSeconds); //每一分钟记录上传一次学时 if (seconds%60 == 0) { //生成新的trainNum 先取之前的(签到时候的或者上一分钟的) 后四位序列码+1。顺便存入userDefault NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; int OrderNum = [[[[userDefaults valueForKey:@"mnTrainOrderNum"] componentsSeparatedByString:@","] lastObject] intValue] + 1; NSString *trainString = @""; if (OrderNum < 10) { trainString = [NSString stringWithFormat:@"000%d",OrderNum]; }else if (OrderNum < 100){ trainString = [NSString stringWithFormat:@"00%d",OrderNum]; }else if (OrderNum < 1000){ trainString = [NSString stringWithFormat:@"0%d",OrderNum]; }else{ trainString = [NSString stringWithFormat:@"%d",OrderNum]; } trainNum = [NSString stringWithFormat:@"%@%@",[trainNum substringToIndex:trainNum.length - 4],trainString]; //将trainOrderNum写入NSUserDefaults trainOrderNum = [NSString stringWithFormat:@"%@,%d",[[[userDefaults valueForKey:@"mnTrainOrderNum"] componentsSeparatedByString:@","] firstObject],OrderNum]; [userDefaults setValue:trainOrderNum forKey:@"mnTrainOrderNum"]; //这个点就当前时间 [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; [minuteTrainDic setValue:dateString forKey:@"gps"];//这里不想新增表字段了 并且正常情况应该是gps里面包含了dateString。这里为了存取方便就。。 [minuteTrainDic setValue:trainNum forKey:@"trainNum"]; NSDictionary *dict = [NSDictionary dictionaryWithDictionary:minuteTrainDic]; [DB_Helper saveTrainWithDic:dict type:mnTableName]; //更新gatherTrainDic数据 并存入本地 [gatherTrainDic setValue:[NSString stringWithFormat:@"%d",(int)photoSeconds] forKey:@"photoTime"]; [gatherTrainDic setValue:trainNum forKey:@"trainNum"]; [self saveGatherTrainDic]; minuteTrainArray = [DB_Helper quearyTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; [self uploadAppTrainPlWithSignOut:NO]; } //更新UI上的时间 [self upDateTime]; } - (void)getClassIdAndSoOn { //classId NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970]*1000; // NSString *timeString = [[[[NSString stringWithFormat:@"%f",timeInterval] substringFromIndex:1] componentsSeparatedByString:@"."] firstObject]; // NSInteger redomNum = arc4random()%8999 + 1000; // classId = [NSString stringWithFormat:@"%d%@",(int)redomNum,timeString]; //13位整数+.+小数。其中第十位刚好是秒 classId = [[NSString stringWithFormat:@"%lf",timeInterval] substringToIndex:10]; //课程编码 classCode NSString *carTypeString = @"21"; if ([defUser.userDict[@"carType"] isEqualToString:@"A1"]) { carTypeString = @"01"; } if ([defUser.userDict[@"carType"] isEqualToString:@"A2"]) { carTypeString = @"02"; } if ([defUser.userDict[@"carType"] isEqualToString:@"A3"]) { carTypeString = @"03"; } if ([defUser.userDict[@"carType"] isEqualToString:@"B1"]) { carTypeString = @"11"; } if ([defUser.userDict[@"carType"] isEqualToString:@"B2"]) { carTypeString = @"12"; } if ([defUser.userDict[@"carType"] isEqualToString:@"C1"]) { carTypeString = @"21"; } if ([defUser.userDict[@"carType"] isEqualToString:@"C2"]) { carTypeString = @"22"; } if ([defUser.userDict[@"carType"] isEqualToString:@"C3"]) { carTypeString = @"23"; } if ([defUser.userDict[@"carType"] isEqualToString:@"C4"]) { carTypeString = @"24"; } if ([defUser.userDict[@"carType"] isEqualToString:@"C5"]) { carTypeString = @"25"; } if ([defUser.userDict[@"carType"] isEqualToString:@"D"]) { carTypeString = @"31"; } if ([defUser.userDict[@"carType"] isEqualToString:@"E"]) { carTypeString = @"32"; } if ([defUser.userDict[@"carType"] isEqualToString:@"F"]) { carTypeString = @"33"; } if ([defUser.userDict[@"carType"] isEqualToString:@"M"]) { carTypeString = @"41"; } if ([defUser.userDict[@"carType"] isEqualToString:@"N"]) { carTypeString = @"42"; } if ([defUser.userDict[@"carType"] isEqualToString:@"P"]) { carTypeString = @"43"; } classCode = [NSString stringWithFormat:@"3%@%@120000",carTypeString,subject]; //学时编号 trainNum NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; [formatter setDateFormat:@"yyMMdd"]; NSString *dateString = [formatter stringFromDate:[NSDate date]]; NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; trainOrderNum = [userDefaults valueForKey:@"mnTrainOrderNum"]; //170209,20 //![trainOrderNum isKindOfClass:[NSString class]] //判断是否是同一天 if (trainOrderNum == nil || ![[[trainOrderNum componentsSeparatedByString:@","] firstObject] isEqualToString:dateString]) { [userDefaults setValue:[NSString stringWithFormat:@"%@,0",dateString] forKey:@"mnTrainOrderNum"]; } trainOrderNum = [userDefaults valueForKey:@"mnTrainOrderNum"]; NSString *trainString = @""; int trainInt = [[[trainOrderNum componentsSeparatedByString:@","] lastObject] intValue]; if (trainInt < 10) { trainString = [NSString stringWithFormat:@"000%d",trainInt]; }else if (trainInt < 100){ trainString = [NSString stringWithFormat:@"00%d",trainInt]; }else if (trainInt < 1000){ trainString = [NSString stringWithFormat:@"0%d",trainInt]; }else{ trainString = [NSString stringWithFormat:@"%d",trainInt]; } trainNum = [NSString stringWithFormat:@"%@%@%@",mnqDev.devnum,dateString,trainString]; } #pragma mark 网络请求 //获取蓝牙名/序列号 - (void)getBlueToothAndDevOrder{ NSMutableArray *arr=[NSMutableArray array]; [arr addPro:@"stuId" Value:defUser.userDict[@"outId"]]; [arr addPro:@"dqbh" Value:defUser.userDict[@"city"]]; NSString* method = @"getStuCardIdOrBluetooth"; [MBProgressHUD showMessage:@"正在获取模拟设备信息" toView:self.view]; [jiaPeiManager requestAnythingWithURL:method array:arr data:nil completion:^(NSDictionary * root) { [MBProgressHUD hideHUDForView:self.view]; if (!root) { ShowMsg(@"请求失败"); _currentStep.status = ClickStatusHighlighted; return; } if (![root[@"code"] isEqualToString:@"0"]) { showMsgByAlert(self, root[@"msg"]); _currentStep.status = ClickStatusHighlighted; return; } //获取到蓝牙名(序列号)搜索蓝牙连接 NSDictionary *dic = root[@"body"]; if (![dic isKindOfClass:[NSDictionary class]]) { ShowMsg(@"数据异常"); _currentStep.status = ClickStatusHighlighted; return; } mnqDev = [[MNQDev alloc]init]; mnqDev.devnum = dic[@"devnum"]; mnqDev.cardId = dic[@"cardId"]; mnqDev.carnum = dic[@"carnum"]; mnqDev.inscode = dic[@"inscode"]; mnqDev.mnqSn = dic[@"mnqSn"]; mnqDev.imei = dic[@"imei"]; peripheralName = mnqDev.carnum; [self searchBlueTooth]; }]; } //判断模拟设备版本号,是否需要升级 -(void)UpdateMNDev{ NSMutableArray *arr=[NSMutableArray array]; [arr addPro:@"versionCode" Value:nowVersion]; [arr addPro:@"dqbh" Value:defUser.userDict[@"city"]]; [arr addPro:@"imei" Value:mnqDev.imei]; [arr addPro:@"devnum" Value:mnqDev.devnum]; [arr addPro:@"inscode" Value:@""]; NSString* method = @"upgradesVersion"; [MBProgressHUD showMessage:@"正在检查模拟设备版本信息" toView:self.view]; [jiaPeiManager requestAnythingWithURL:method array:arr data:nil completion:^(NSDictionary * root) { [MBProgressHUD hideHUDForView:self.view]; if (!root) { _currentStep.status = ClickStatusHighlighted; [self addBlueToothLabelWithContent:@"模拟设备更新检查失败" mp3:nil]; ShowMsg(@"请求失败"); return; } if ([root[@"code"] isEqualToString:@"-1"]) {//这里和jiapeimanager里面的-1处理冲突了。就不管它吧,不是很重要。 _currentStep.status = ClickStatusHighlighted; [self addBlueToothLabelWithContent:@"模拟设备更新检查失败" mp3:nil]; ShowMsg(root[@"msg"]); return; } if ([root[@"code"] isEqualToString:@"1"]) { //不需要更新 self.currentStep = _step3; blueToothOrder = kGetDevNumAndStuCardOrder; [self sendBlueToothOrder]; return; } //检测到新版本 NSString *urlStr = root[@"body"]; if ([root[@"code"] isEqualToString:@"0"]) { if (urlStr && urlStr.length != 0) { [self addBlueToothLabelWithContent:@"模拟设备升级中" mp3:nil]; [self downLoadNewDataWithurl:urlStr]; } } }]; } -(void)downLoadNewDataWithurl:(NSString *)urlStr{ //创建传话管理者 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]]; //下载文件 /* 第一个参数:请求对象 第二个参数:progress 进度回调 第三个参数:destination 回调(目标位置) 有返回值 targetPath:临时文件路径 response:响应头信息 第四个参数:completionHandler 下载完成后的回调 filePath:最终的文件路径 */ NSURLSessionDownloadTask *download = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) { //下载进度 dispatch_async(dispatch_get_main_queue(), ^{ MBProgressHUD *hud = [MBProgressHUD HUDForView:self.view]; hud.label.text = [NSString stringWithFormat:@"正在下载模拟设备更新包(%.2f%%)",100.0 * (downloadProgress.completedUnitCount / downloadProgress.totalUnitCount)]; }); NSLog(@"%.2f",1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount); } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { //保存的文件路径 NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename]; return [NSURL fileURLWithPath:fullPath]; } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) { if (error) { _currentStep.status = ClickStatusHighlighted; NSLog(@"下载更新包出错:%@",error); dispatch_async(dispatch_get_main_queue(), ^{ [MBProgressHUD hideHUDForView:self.view]; showMsgByAlert(self, @"下载更新包出错"); }); return; } NSLog(@"升级包filePath: %@",filePath); if (filePath) { [self startUpdateMNQDevWithFilePath:filePath]; } }]; //执行Task dispatch_async(dispatch_get_main_queue(), ^{ [MBProgressHUD showMessage:@"正在下载模拟设备更新包" toView:self.view]; }); [download resume]; } //人脸比对签到face2face/过程验证faceComparison onProcessVerification == yes 过程验证 - (void)upLoadPhotoWithImage:(UIImage *)image{ //蓝牙照片不作处理(调整图片像素) UIImage *cutImg; if (peripheralName.length > 0) { cutImg = image; }else { //剪切 然后压缩图片 NSData *data = UIImageJPEGRepresentation([image scaledAndCutToSize:CGSizeMake(240, 320)],0.5); cutImg = [UIImage imageWithData:data]; } if (!myDelegate.isBackgroundTask) { if (!_photoImg) { _photoImg = [[UIImageView alloc] initWithFrame:CGRectMake(10, kSize.height - kNavOffSet - 200 - kSafeAreaBottomHeight, 200, 150)]; _photoImg.userInteractionEnabled = YES; UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 40)]; [label setText:@"点击移除照片" Font:Font17 TextColor:[UIColor orangeColor] Alignment:NSTextAlignmentCenter]; [_photoImg addSubview:label]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(removeImage)]; [_photoImg addGestureRecognizer:tap]; } [self.view addSubview:_photoImg]; _photoImg.image = cutImg; [self performSelector:@selector(removeImage) withObject:nil afterDelay:10]; } NSMutableArray *arr=[NSMutableArray array]; [arr addPro:@"dqbh" Value:defUser.userDict[@"city"]]; [arr addPro:@"photo" Value:[UIImageJPEGRepresentation(cutImg,1.0) base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]];//不带水印照片 [arr addPro:@"lng" Value:[NSString stringWithFormat:@"%f",myCoordinate.longitude]]; [arr addPro:@"lat" Value:[NSString stringWithFormat:@"%f",myCoordinate.latitude]]; [arr addPro:@"stuId" Value:defUser.userDict[@"outId"]]; [arr addPro:@"subject" Value:subject]; [arr addPro:@"subjcode" Value:classCode]; [arr addPro:@"classId" Value:classId]; NSString *method = onProcessVerification ? @"faceComparison" : @"face2face"; [MBProgressHUD showMessage:@"正在进行人脸比对" toView:self.view]; [jiaPeiManager requestAnythingWithURL:method array:arr data:nil completion:^(NSDictionary * root) { [MBProgressHUD hideHUDForView:self.view animated:YES]; blueToothOrder = @""; if (!root) { _currentStep.status = ClickStatusHighlighted; if (![Util connectedToNetWork]) { blueToothOrder = @""; showMsgByAlert(self, @"请检查手机网络连接是否正常"); return; } [Tools playAudioWithString:@"人脸比对请求失败"]; UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:@"人脸比对请求失败" preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { _currentStep.status = ClickStatusHighlighted; }]]; [alertFind addAction:[UIAlertAction actionWithTitle:@"再试一次" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { _currentStep.status = ClickStatusMid; [self upLoadPhotoWithImage:image]; }]]; [self presentViewController:alertFind animated:true completion:nil]; return; } if ([root[@"code"] isEqualToString:@"90"]) { UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:root[@"msg"] preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [LoginViewController loginFromVC:self]; }]]; [self presentViewController:alertFind animated:true completion:nil]; return; } if ([root[@"code"] isEqualToString:@"1"]) { [Tools playAudioWithString:root[@"msg"]]; [self addBlueToothLabelWithContent:[NSString stringWithFormat:@"人脸验证失败"] mp3:nil]; if (onProcessVerification) { //判断次数==0表示不限制失败次数,这个次数限制似乎就只有防止恶意调用接口的作用,减轻服务器压力? if (processVerificationMaxCount != 0) { processVerificationFailureCount++; //达到最大次数 if (processVerificationFailureCount == processVerificationMaxCount) { [self processVerificationSignOut]; return; } } } [self showAlertTakePhotoFailWithStr:root[@"msg"]]; return; } //过程验证不存在这种情况。就不管吧。但是为了防止服务器给一个惊喜 还是判断一下 //code = 2;已经签到成功了(可能因为上一次请求网络延迟,服务器已经签到成功,手机没有get到这个信息) if (onProcessVerification && [root[@"code"] isEqualToString:@"2"]) { [self getCurrentState]; return; } //code = 0;正常成功状态。 if ([root[@"code"] isEqualToString:@"0"]) { if (onProcessVerification) { //过程人脸比对成功 恢复计时; photoSeconds = 0;//通过验证 onProcessVerification = NO; self.currentStep = _step5; //过程中不需要再发送重要指令。该定时器主要处理拍照命令 if (signInTimer) { [signInTimer invalidate]; signInTimer = nil; } [self addBlueToothLabelWithContent:@"过程验证成功,恢复计时" mp3:nil]; [Tools playAudioWithString:[NSString stringWithFormat:@"过程验证成功,恢复计时"]]; }else{ //code = 0;正常成功状态。 //更新数据 [gatherTrainDic setValue:root[@"body"][@"CreditRation"] forKey:@"allNeed"];//总需学时 [gatherTrainDic setValue:root[@"body"][@"DurationSum"] forKey:@"allCompelet"];//已完成学时 CGFloat seconds = [[NSString stringWithFormat:@"%@",root[@"body"][@"Duration"]] floatValue]*60; [gatherTrainDic setValue:[NSString stringWithFormat:@"%f",seconds] forKey:@"allTime"];//当天学时 [self saveGatherTrainDic]; //人脸比对成功服务器签到 开始计时 [self beginTimer]; } } }]; } - (void)processVerificationSignOut{ self.currentStep = _step6; //先关闭定时器 if (timer.isValid) { [timer invalidate]; timer = nil; myDelegate.mnPeriodVC = nil; } //关闭蓝牙连接 [self closeBlueTooth]; [gatherTrainDic setValue:@"2" forKey:@"isOver"]; [self saveGatherTrainDic]; minuteTrainArray = [DB_Helper quearyTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; if (minuteTrainArray.count > 0) { [self uploadAppTrainPlWithSignOut:YES]; }else{ [self signOut]; } } //上传分钟学时(批量) 这里要判断一下是否是最后签退过来的要做上传提示 - (void)uploadAppTrainPlWithSignOut:(BOOL)isSignOut { if (![Util connectedToNetWork]) { if (isSignOut) { _step6.status = ClickStatusHighlighted; showMsgUnconnect(); } [dateFormatter setDateFormat:@"HH:mm"]; NSString *timeString = [dateFormatter stringFromDate:[NSDate date]]; [dataSourceArray addObject:[NSString stringWithFormat:@"1%@ 学时上传失败!",timeString]]; [mainTableView reloadData]; return; } NSMutableArray *mArr = [NSMutableArray new];; for (NSDictionary *dic in minuteTrainArray) { NSMutableDictionary * csDic = [NSMutableDictionary new]; [csDic setValue:defUser.userDict[@"outId"] forKey:@"stuId"]; [csDic setValue:dic[@"trainNum"] forKey:@"trainNo"]; [csDic setValue:dic[@"classCode"] forKey:@"subjcode"]; [csDic setValue:dic[@"classId"] forKey:@"classId"]; [csDic setValue:dic[@"gps"] forKey:@"recordTime"]; [csDic setValue:defUser.userDict[@"city"] forKey:@"dqbh"]; [mArr addObject:csDic]; } NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:mArr options:NSJSONWritingPrettyPrinted error:&error]; NSString *jsonString = nil; if ([jsonData length] != 0 && error == nil){ // 使用这个方法的返回,我们就可以得到想要的JSON串 jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; }else{ NSLog(@"数据解析异常:%@",error); } NSMutableArray *arr=[NSMutableArray array]; [arr addPro:@"trains" Value:jsonString]; NSString* method = @"uploadTrainMinPl"; if (isSignOut) { [MBProgressHUD showMessage:@"正在补传学时" toView:self.view]; [self addBlueToothLabelWithContent:@"正在补传学时" mp3:nil]; } NSString *timeString = [dateFormatter stringFromDate:[NSDate date]]; [jiaPeiManager requestAnythingWithURL:method array:arr data:nil completion:^(NSDictionary * root) { if (isSignOut) { [MBProgressHUD hideHUDForView:self.view animated:YES]; } [self.view setUserInteractionEnabled:YES]; [dateFormatter setDateFormat:@"HH:mm"]; if (!root) { if (isSignOut) { _currentStep.status = ClickStatusHighlighted; [Tools playAudioWithString:@"学时补传失败"]; [self addBlueToothLabelWithContent:@"学时补传失败" mp3:nil]; } [dataSourceArray addObject:[NSString stringWithFormat:@"1%@ 学时上传失败!",timeString]]; return ; } if ([root[@"code"] isEqualToString:@"1"]) { if (isSignOut) { _currentStep.status = ClickStatusHighlighted; [Tools playAudioWithString:root[@"msg"]]; showMsgByAlert(self, root[@"msg"]); [self addBlueToothLabelWithContent:[NSString stringWithFormat:@"学时补传失败:%@",root[@"msg"]] mp3:nil]; } [dataSourceArray addObject:[NSString stringWithFormat:@"1%@ 学时上传失败!",timeString]]; return; } if ([root[@"code"] isEqualToString:@"2"]) {//超过规定时间 服务器通知签退 [self haveSignOutByUser:NO]; return; } [dataSourceArray addObject:[NSString stringWithFormat:@"0%@ 学时上传成功!",timeString]]; [DB_Helper deleteTrainWithClassId:gatherTrainDic[@"classId"] type:mnTableName]; if (isSignOut) { [self addBlueToothLabelWithContent:@"学时补传成功" mp3:nil]; [self signOut]; }else{ [mainTableView reloadData]; } }]; } - (void)signOut{ if (![Util connectedToNetWork]) { _step6.status = ClickStatusHighlighted; showMsgUnconnect(); return; } NSMutableArray *arr=[NSMutableArray array]; [arr addPro:@"stuId" Value:defUser.userDict[@"outId"]]; [arr addPro:@"dqbh" Value:defUser.userDict[@"city"]]; [arr addPro:@"subjcode" Value:classCode]; [arr addPro:@"classId" Value:classId];//签退如果没有这个东西 这段学时会无效 [arr addPro:@"lng" Value:[NSString stringWithFormat:@"%f",myCoordinate.longitude]]; [arr addPro:@"lat" Value:[NSString stringWithFormat:@"%f",myCoordinate.latitude]]; NSString* method = @"uploadMnqSignOut"; [MBProgressHUD showLoadToView:self.view]; [jiaPeiManager requestAnythingWithURL:method array:arr data:nil completion:^(NSDictionary * root) { [MBProgressHUD hideHUDForView:self.view animated:YES]; if (!root) { _step6.status = ClickStatusHighlighted; ShowMsg(@"请求失败"); [Tools playAudioWithString:@"请求失败"]; return ; } if ([root[@"code"] isEqualToString:@"90"]) {//缺少outid UIAlertController *alertFind = [UIAlertController alertControllerWithTitle:nil message:root[@"msg"] preferredStyle:UIAlertControllerStyleAlert]; [alertFind addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [LoginViewController loginFromVC:self]; }]]; [self presentViewController:alertFind animated:true completion:nil]; return; } if ([root[@"code"] isEqualToString:@"1"]) { _step6.status = ClickStatusHighlighted; ShowMsg(root[@"msg"]); [Tools playAudioWithString:root[@"msg"]]; return; } [self haveSignOutByUser:YES]; }]; } //获取当前训练状态; -(void)getCurrentState { [MBProgressHUD showMessage:@"正在获取当前训练状态" toView:self.view]; //这个主要目的是为了更新本地学时编号等数据 [LoginViewController getCurrentStateWithCompleteBlock:^(NSDictionary *root) { [MBProgressHUD hideHUDForView:self.view animated:NO]; if (!root) { ShowMsg(@"请求失败"); return; } if ([root[@"code"] isEqualToString:@"0"]) { /*模拟训练状态判断 0 成功获取学员信息 1 学员还未在模拟器签到 2.已登录,未签到(未进行人脸验证) 3 无法获取到当前训练科目(人脸比对验证通过,但是没有上传任何学时) 4 隔日签到记录 5 分钟学时科目异常 */ NSString *status = [NSString stringWithFormat:@"%@",root[@"body"][@"mn"][@"TYPE"]]; //获取到状态 if ([status integerValue] == 0) { [self beginTimer]; }else{ //既然人脸比对结果提示已经在训练中了 就不该有其它状态,如果还是发生了,就作比对结果返回异常处理 [self showAlertTakePhotoFailWithStr:@"异常情况"]; } }else{ ShowMsg(root[@"msg"]); } }]; } #pragma mark - 签退成功后的操作 - (void)haveSignOutByUser:(BOOL)isByUser{ if (isByUser == NO) {//如果不是用户操作的签退 self.currentStep = _step6; //先关闭定时器 if (timer.isValid) { [timer invalidate]; timer = nil; myDelegate.mnPeriodVC = nil; } //关闭蓝牙连接 [self closeBlueTooth]; [gatherTrainDic setValue:@"2" forKey:@"isOver"]; [self saveGatherTrainDic]; } //语音提示 NSString *alertMessage = @"模拟计时已签退,本次计时结束"; if (processVerificationFailureCount == processVerificationMaxCount && processVerificationFailureCount != 0) { alertMessage = [NSString stringWithFormat:@"本次计时,过程验证失败次数%ld次,计时结束",processVerificationFailureCount]; [self addBlueToothLabelWithContent:alertMessage mp3:@"mn_blueToothQuit.mp3"]; }else{ [self addBlueToothLabelWithContent:@"模拟计时已签退,本次计时结束" mp3:@"mn_blueToothQuit.mp3"]; } [DB_Helper deleteAllTrainWithType:mnTableName]; //将本地状态置为结束 [gatherTrainDic setValue:@"1" forKey:@"isOver"]; [gatherTrainDic removeObjectForKey:@"classId"]; [gatherTrainDic removeObjectForKey:@"subject"]; [self saveGatherTrainDic]; //关闭学时状态显示 [dataSourceArray removeAllObjects]; [mainTableView reloadData]; trainView.hidden = YES; //timeLabel.text = @"00:00:00" ; //先关闭定时器 if (timer.isValid) { [timer invalidate]; timer = nil; } myDelegate.mnPeriodVC = nil; myDelegate.mnTrainType = @"1"; //清空detailLab(top) //for (UILabel *detailLab in detailLabArr) { //detailLab.text = @""; //} UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"提醒" message:[NSString stringWithFormat:@"%@.\n(ps:手机蓝牙可以关掉啦)",alertMessage] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil]; servicesDisabledAlert.delegate = self; [servicesDisabledAlert show]; } //仅针对“手机蓝牙可以关掉啦” ->只有它设置了代理为self才会触发下面的方法 ->退出当前页面 -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ [self goBackByNavigation];//如果不是当前vc,测试也ok } #pragma mark - 转json字符串 - (NSString *)getJsonStringWithObj:(NSObject *)jsonObj{ NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObj options:NSJSONWritingPrettyPrinted error:&error]; NSString *jsonString = nil; if ([jsonData length] != 0 && error == nil){ // 使用这个方法的返回,我们就可以得到想要的JSON串 jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; }else{ ShowMsg(@"数据错误,请重试!"); } return jsonString; } @end