rt300@0: // rt300@0: // eventLogger.mm rt300@0: // oscSenderExample rt300@0: // rt300@0: // Created by Robert Tubb on 05/11/2012. rt300@0: // rt300@0: // rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: #include "eventLogger.h" rt300@0: rt300@0: EventLogger eventLogger; rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: EventLogger::EventLogger(){ rt300@0: rt300@0: consentGiven = true; // unless told otherwise firstAppOpen rt300@0: loggingEnabled = true; rt300@0: serverConnectionOK = false; rt300@0: questionnaireCompleted = false; rt300@0: questionnaireUploaded = false; rt300@0: logUploadInProgress = false; rt300@0: rt300@0: nextUploadQty = UPLOAD_CHUNK_SIZE; // amount of data uploaded is always more than UPLOAD_CHUNK_SIZE events rt300@0: serverComms = [[ServerComms alloc] init]; rt300@0: rt300@0: } rt300@18: // for RIFTATHON this replaces startLoadAll rt300@18: // because we can't look at logs or presets until we know user rt300@18: rt300@18: void EventLogger::onUsernameEntered(){ rt300@18: sessionStartTime = ofGetSystemTime(); rt300@18: // userName = name; done already by dialog rt300@18: // check for eventlog with this name rt300@18: rt300@18: rt300@18: string path = ofxiPhoneGetDocumentsDirectory(); rt300@18: ofDirectory dir(path); rt300@18: //only show png files rt300@18: dir.allowExt("json"); rt300@18: //populate the directory object rt300@18: dir.listDir(); rt300@18: rt300@18: //chek for username's logs rt300@18: bool userHasLogs = false; rt300@18: int numExistingLogs = 0; rt300@18: for(int i = 0; i < dir.numFiles(); i++){ rt300@18: string fname = dir.getName(i); rt300@18: if( 0 == fname.compare(0, userName.length() + 10, userName + "_eventlog_" )){ rt300@18: numExistingLogs++; rt300@18: userHasLogs = true; rt300@18: } rt300@18: rt300@18: } rt300@18: // if found, load other details such as device id rt300@18: loadUserDetailsFromLastLogFile(numExistingLogs); rt300@18: nextLogFileIndex = numExistingLogs+1; rt300@18: rt300@18: } rt300@18: rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::startLoadAll(){ rt300@0: rt300@0: // TODO pilot doesn't check existing rt300@0: //loadExistingLogFile(ofxiPhoneGetDocumentsDirectory() + EVENT_LOG_FILENAME); rt300@0: rt300@0: rt300@0: sessionStartTime = ofGetSystemTime(); rt300@0: rt300@0: // TODO pilot rt300@0: // testConnection(); rt300@0: rt300@0: // if we have a back log of events upload them rt300@0: if(theEvents.size() > nextUploadQty && ! logUploadInProgress){ rt300@0: //try to upload rt300@0: rt300@0: // TODO pilot doesn't upload rt300@0: //uploadEventLog(true); rt300@0: } rt300@0: rt300@0: if(questionnaireCompleted && !questionnaireUploaded){ rt300@0: uploadQuestionnaire(); rt300@0: } rt300@0: rt300@0: //TODO if questionnaire wasn't reached but still opened then set the timer to something sensible rt300@0: rt300@0: //timer.setInteractionTime(savedInteractionTime); rt300@0: //timer.setOrderFromPrevious(interfaceOrder); rt300@0: rt300@0: // for now sod it, start from scratch rt300@0: if(!questionnaireCompleted){ rt300@0: cout<<"Questionnaire NOT completed - first APP open called\n"; rt300@0: firstEverAppOpen(); rt300@0: } rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::testConnection(){ rt300@0: Json::Value root; rt300@0: root["x"] = "y"; rt300@0: cout << "testConnection\n"; rt300@0: sendToServer("testConnection", root, true); rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@18: rt300@18: void EventLogger::loadUserDetailsFromLastLogFile(int numExistingLogs){ rt300@18: rt300@18: rt300@18: } rt300@18: //--------------------------------------------------------------------------- rt300@18: rt300@18: rt300@0: // this reads the persistent log file , checks if we've used the app before and rt300@0: // if we've answered questionnaire or not rt300@0: rt300@0: // should just store stuff. the start up state should be handled by testAPp rt300@0: rt300@0: void EventLogger::loadExistingLogFile(const string &jsonFile){ rt300@0: Json::Value root; rt300@0: Json::Reader reader; rt300@0: rt300@0: ///////////// rt300@0: // read file rt300@0: rt300@0: ifstream theFile(jsonFile.c_str()); rt300@0: stringstream fileText; rt300@0: string line; rt300@0: if(!theFile){ rt300@0: cout<<"No event log file found - first APP open called\n"; rt300@0: firstEverAppOpen(); rt300@0: return; rt300@0: }else{ rt300@0: while(theFile){ rt300@0: theFile >> line; rt300@0: // cout << line; // lots!!!! rt300@0: fileText << line; rt300@0: } rt300@0: theFile.close(); rt300@0: } rt300@0: rt300@0: cout << "size of log JSON string:" << fileText.str().length() << "BYTES \n"; rt300@0: rt300@0: bool parsingSuccessful = reader.parse( fileText.str(), root ); rt300@0: rt300@0: if ( !parsingSuccessful ) rt300@0: { rt300@0: // report to the user the failure and their locations in the document. rt300@0: std::cout << "Failed to parse event log JSON: \n" rt300@0: << reader.getFormattedErrorMessages(); rt300@0: return; rt300@0: } rt300@0: rt300@0: ///////////////// rt300@0: // now put user deets into variables rt300@0: rt300@0: userName = root["userName"].asString(); rt300@0: deviceID = root["deviceID"].asLargestInt(); rt300@0: nextUploadNumber = root["uploadNumber"].asInt(); rt300@0: savedInteractionTime = root["savedInteractionTime"].asLargestInt(); rt300@0: questionnaireCompleted = root["questionnaireCompleted"].asBool(); rt300@0: questionnaireUploaded = root["questionnaireUploaded"].asBool(); rt300@0: rt300@0: // check for unuploaded evts rt300@0: const Json::Value jlogs = root["events"]; rt300@0: rt300@0: for ( int index = 0; index < jlogs.size(); ++index ) theEvents.push_back(lEvent(jlogs[index])); rt300@0: rt300@0: rt300@0: ////////////// rt300@0: rt300@0: if(questionnaireCompleted && !questionnaireUploaded){ rt300@0: // then read it in and upload it rt300@0: Json::Value JQ = root["questionnaire"]; rt300@0: Json::Value JArray = JQ["qAnswers"]; rt300@0: if(JArray.size() < 2){ rt300@0: cout << "Error - status of questionnaire is wierd\n"; rt300@0: } rt300@0: for ( unsigned int i = 0; i < JArray.size(); i++ ) rt300@0: { rt300@0: questionnaireAnswers.push_back(JArray[i].asInt()); rt300@0: } rt300@0: questionnaireComments = JQ["comments"].toStyledString(); rt300@0: //uploadQuestionnaire(); rt300@0: } rt300@0: rt300@0: // TODO interaction time seems to be balls rt300@0: cout << "Total interaction time: " << savedInteractionTime << '\n'; rt300@0: rt300@0: } rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: rt300@0: void EventLogger::firstEverAppOpen(){ rt300@0: rt300@0: nextUploadNumber = 0; rt300@0: deviceID = ofGetSystemTimeMicros(); rt300@0: savedInteractionTime = 0; rt300@0: questionnaireCompleted = false; rt300@0: questionnaireUploaded = false; rt300@0: rt300@0: } rt300@0: rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::questionnaireAnswersObtained(vector answers, const char* userComments){ rt300@0: rt300@0: questionnaireCompleted = true; rt300@0: questionnaireAnswers = answers; rt300@0: questionnaireComments = userComments; rt300@0: uploadQuestionnaire(); rt300@0: logEvent(QUESTIONNAIRE_COMPLETED); rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::uploadQuestionnaire(){ rt300@0: // show indicator rt300@0: cout << "^^^^^^^^ UPLOADING QUESTIONNAIRE ^^^^^^^^ \n"; rt300@0: cout << questionnaireToJson() << "\n"; rt300@0: sendToServer("questionnaire", questionnaireToJson(), true); rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: bool EventLogger::sendToServer(string functionName, Json::Value jsonData, bool async = false){ rt300@0: rt300@0: Json::FastWriter writer; rt300@0: string jsontext = writer.write( jsonData ); rt300@0: rt300@0: // remove newline rt300@0: if (!jsontext.empty() && jsontext[jsontext.length()-1] == '\n') { rt300@0: jsontext.erase(jsontext.length()-1); rt300@0: } rt300@0: ostringstream jd; rt300@0: jd << jsontext; rt300@0: NSString *theData = [NSString stringWithUTF8String:jd.str().c_str()]; rt300@0: NSString *theType = [NSString stringWithUTF8String:functionName.c_str()]; rt300@0: rt300@0: if(async){ rt300@0: [serverComms doPostRequest:theType withData:theData]; rt300@0: }else{ rt300@0: bool success = [serverComms doSyncPostRequest:theType withData:theData]; rt300@0: return success; rt300@0: } rt300@0: rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::questionnaireOK(){ rt300@0: questionnaireUploaded = true; rt300@0: questionnaireComments = ""; rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::eventlogOK(){ rt300@0: // COMMENT THIS IF UPLAODING FROM IPAD TO XCODE rt300@0: rt300@0: // it's a bad idea to do this in another thread... rt300@0: theEvents.clear(); rt300@0: cout << "EVENT LOG UPLOAD SUCCESS\n"; rt300@0: nextUploadNumber++; rt300@0: logUploadInProgress = false; rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::testConnectionOK(){ rt300@0: cout << "^^^^^^^^ server connection OK ^^^^^^^^ \n"; rt300@0: serverConnectionOK = true; rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::questionnaireNotOK(){ rt300@0: cout << "XXXXX questionnaire NOT OK XXXXXXX \n"; rt300@0: questionnaireUploaded = false; rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::eventlogNotOK(){ rt300@0: // try later rt300@0: cout << "XXXXX event log NOT OK XXXXXXX \n"; rt300@0: nextUploadQty += UPLOAD_CHUNK_SIZE; rt300@0: logUploadInProgress = false; rt300@0: } rt300@0: //----------------------------- rt300@0: void EventLogger::testConnectionNotOK(){ rt300@0: cout << "XXXXX server connection NOT OK XXXXXXX \n"; rt300@0: serverConnectionOK = false; rt300@0: // alert? rt300@0: rt300@0: } rt300@0: rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: rt300@0: bool EventLogger::uploadEventLog(bool async){ rt300@0: rt300@0: // show indicator rt300@0: logUploadInProgress = true; rt300@0: cout << "^^^^^^^^ ATTEMPTING TO UPLOAD " << theEvents.size() << " EVENTS ^^^^^^^^ .\n"; rt300@0: if(!async){ rt300@0: bool success = sendToServer("eventlog", logsToJson(), async); rt300@0: if(!success){ rt300@0: // try later rt300@0: nextUploadQty += UPLOAD_CHUNK_SIZE; rt300@0: // increment upload number? rt300@0: }else{ rt300@0: rt300@0: // if success - clear memory rt300@0: // IF UPLAODING FROM IPAD TO XCODE COMMENT OUT rt300@0: theEvents.clear(); rt300@0: cout << "UPLOAD SUCCESS\n"; rt300@0: nextUploadNumber++; rt300@0: } rt300@0: logUploadInProgress = false; rt300@0: return success; rt300@0: }else{ rt300@0: sendToServer("eventlog", logsToJson(), async); rt300@0: } rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: // only called when doing supervised tests rt300@0: void EventLogger::newUser(){ rt300@0: // store old stuff rt300@0: rt300@0: saveSessionToFile(); rt300@0: cout<<"setup new user\n"; rt300@0: deleteLogs(); rt300@0: nextUploadNumber = 0; rt300@0: deviceID = ofGetSystemTimeMicros(); rt300@0: savedInteractionTime = 0; rt300@0: totalInteractionTime = 0; rt300@0: sessionStartTime = ofGetSystemTime(); rt300@0: questionnaireCompleted = false; rt300@0: questionnaireUploaded = false; rt300@0: rt300@0: //((testApp *)ofGetAppPtr())->showIntro(); rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: // called from alertView OK in iViewController rt300@0: void EventLogger::setUsername(const char *u){ rt300@0: userName = u; rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: //new tweakathlon event logger with vector rt300@0: void EventLogger::logEvent(const leventType& evtType, const vector eventData){ rt300@0: Poco::Mutex::ScopedLock lock(_mutex); rt300@0: if(!loggingEnabled) return; rt300@0: rt300@0: switch ( evtType ) { rt300@0: rt300@0: // case CANDIDATE_PARAM_ADJUSTED: rt300@0: // // TODO thinning here. maybe. was a pain in the butt. rt300@0: // thinnedSliderEvent(lEvent(evtType,eventData)); rt300@0: // break; rt300@0: default: rt300@0: // default is just an event type with vector of ints. matlab will know what they are. rt300@0: rt300@0: rt300@0: theEvents.push_back(lEvent(evtType, eventData)); rt300@0: rt300@0: break; rt300@0: } rt300@0: rt300@18: // new riftathon save as we go rt300@18: if (theEvents.size() > SAVE_CHUNK_SIZE){ rt300@18: saveSessionToFile(); rt300@18: theEvents.clear(); rt300@0: } rt300@18: rt300@0: //TODO thiswrong? rt300@0: totalInteractionTime = savedInteractionTime + (ofGetSystemTime() - sessionStartTime); // milliseconds rt300@0: rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: // OLD SONIC ZOOM EVT - still used for simple events with no data rt300@0: void EventLogger::logEvent(const leventType& evtType){ rt300@0: Poco::Mutex::ScopedLock lock(_mutex); rt300@0: if(!loggingEnabled) return; rt300@0: rt300@0: switch ( evtType ) { rt300@0: // data thinning here rt300@0: case APP_LOADED: rt300@0: theEvents.push_back(lEvent(evtType)); rt300@0: default: rt300@0: // default is just an event type with vector of ints. matlab will know what they are. rt300@0: rt300@0: rt300@0: theEvents.push_back(lEvent(evtType)); rt300@0: rt300@0: } rt300@0: rt300@0: if(theEvents.size() > nextUploadQty && !logUploadInProgress){ rt300@0: //try to upload asynchronously rt300@0: // TODO pilot doesn't upload rt300@0: //uploadEventLog(true); rt300@0: } rt300@18: rt300@34: // new riftathon save as we go NEVER REACHES HERE? rt300@18: if (theEvents.size() > SAVE_CHUNK_SIZE){ rt300@18: saveSessionToFile(); rt300@18: } rt300@18: rt300@0: //TODO thiswrong? rt300@0: totalInteractionTime = savedInteractionTime + (ofGetSystemTime() - sessionStartTime); // milliseconds rt300@0: rt300@0: } rt300@0: // UnaryPredicate rt300@0: template rt300@0: bool EventLogger::matchID(T thing){ rt300@0: return true; rt300@0: } rt300@0: bool EventLogger::matchID2(){ rt300@0: return true; rt300@0: } rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::thinnedSliderEvent(lEvent newEvent){ rt300@0: static vector unloggedEventsCache; // list of all different slider events rt300@0: static int eventCounter = 0; rt300@0: rt300@0: // if first event then log it. rt300@0: if(theEvents.size() == 0){ rt300@0: theEvents.push_back(newEvent); rt300@0: } rt300@0: rt300@0: // look for last event in the cache that had this mappingID rt300@0: cout << unloggedEventsCache.size() << endl; rt300@0: rt300@0: vector::iterator lastMatching; rt300@0: for(lastMatching = unloggedEventsCache.begin(); rt300@0: lastMatching != unloggedEventsCache.end() && ( (*lastMatching).eventData[0] != newEvent.eventData[0]); rt300@0: lastMatching++); rt300@0: rt300@0: rt300@0: if (lastMatching != unloggedEventsCache.end()){ rt300@0: //cout << "matching id logevent: " << (*lastMatching).eventData[0] << " " << newEvent.eventData[0] << endl; rt300@0: // we have another, check gap rt300@0: int gap = newEvent.eventTime - (*lastMatching).eventTime; rt300@0: if(gap > 300){ rt300@0: theEvents.push_back((*lastMatching)); rt300@0: rt300@0: // also log new slider evt rt300@0: theEvents.push_back(newEvent); rt300@0: eventCounter = 0; rt300@0: rt300@0: }else if(eventCounter >= EVENT_THIN_FACTOR){ // otherwise only record every Nth event rt300@0: theEvents.push_back(newEvent); rt300@0: eventCounter = 0; rt300@0: // DELETE THAT PREVIOUS EVENT rt300@0: unloggedEventsCache.erase(lastMatching); rt300@0: rt300@0: }else{ rt300@0: // ignore for now but put in unlogged rt300@0: unloggedEventsCache.push_back(newEvent); rt300@0: } rt300@0: rt300@0: }else{ rt300@0: // this event type wasnt in the cache rt300@0: if(eventCounter >= EVENT_THIN_FACTOR){ // otherwise only record every Nth event rt300@0: theEvents.push_back(newEvent); rt300@0: eventCounter = 0; rt300@0: }else{ rt300@0: unloggedEventsCache.push_back(newEvent); rt300@0: } rt300@0: rt300@0: } rt300@0: // always count the new one rt300@0: eventCounter++; rt300@0: rt300@0: rt300@0: rt300@0: } rt300@0: rt300@0: //-------------------------------------------------------------------- rt300@0: // called from newUser rt300@0: void EventLogger::deleteLogs(){ rt300@18: return; rt300@0: // the rt300@0: theEvents.clear(); rt300@0: string fname = ofxiPhoneGetDocumentsDirectory() + EVENT_LOG_FILENAME; rt300@0: ofFile logFile(fname,ofFile::WriteOnly); rt300@0: logFile << ""; rt300@0: logFile.close(); rt300@0: } rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: rt300@0: Json::Value EventLogger::logsToJson(){ rt300@0: // put all logged events into Json formatted string rt300@0: Json::Value root; rt300@0: rt300@0: vector::iterator eventIter; rt300@0: rt300@0: root["programVersion"] = PROGRAM_VERSION; rt300@0: root["userName"] = userName; rt300@0: root["deviceID"] = deviceID; rt300@0: root["uploadNumber"] = nextUploadNumber; rt300@0: root["savedInteractionTime"] = savedInteractionTime; rt300@0: root["questionnaireCompleted"] = questionnaireCompleted; rt300@0: root["questionnaireUploaded"] = questionnaireUploaded; rt300@0: rt300@0: // this can mess up matlab script if it doesn't find this field rt300@0: root["questionnaire"] = questionnaireToJson(); rt300@0: rt300@0: rt300@0: int i = 0; rt300@0: for(eventIter = theEvents.begin(); eventIter < theEvents.end(); eventIter++){ rt300@0: root["events"][i] = (*eventIter).eventToJson(); rt300@0: i++; rt300@0: } rt300@0: root["numEventsHere"] = i; rt300@0: return root; rt300@0: } rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: rt300@0: Json::Value EventLogger::questionnaireToJson(){ rt300@0: // put all answers into Json formatted string rt300@0: Json::Value root; rt300@0: rt300@0: vector::iterator aIter; rt300@0: rt300@0: Json::Value questionnaire; rt300@0: rt300@0: int i = 0; rt300@0: for(aIter = questionnaireAnswers.begin(); aIter < questionnaireAnswers.end(); aIter++){ rt300@0: questionnaire[i] = (*aIter); rt300@0: i++; rt300@0: } rt300@0: rt300@0: root["qAnswers"] = questionnaire; rt300@0: root["comments"] = questionnaireComments; rt300@0: root["userName"] = userName; rt300@0: root["deviceID"] = deviceID; rt300@0: root["programVersion"] = PROGRAM_VERSION; rt300@0: rt300@0: return root; rt300@0: } rt300@0: rt300@0: //--------------------------------------------------------------------------- rt300@0: void EventLogger::printAll(){ rt300@0: cout << "-----------------ALL LOGGED EVENTS----------------- \n"; rt300@0: vector::iterator evIter; rt300@0: cout << logsToJson() << "\n"; rt300@0: cout << "---------------------QUESTIONNAIRE---------------- \n"; rt300@0: cout << questionnaireToJson() << "\n"; rt300@0: }; rt300@0: //--------------------------------------------------------------------------- rt300@0: rt300@0: void EventLogger::saveSessionToFile(){ rt300@0: stringstream fn; rt300@18: fn << ofxiPhoneGetDocumentsDirectory() << userName << "_eventlog_" << nextLogFileIndexString() << ".json"; rt300@0: rt300@0: string fname = fn.str(); //ofxiPhoneGetDocumentsDirectory() + userName + '_' + "TESTSAVE.json"; rt300@0: rt300@0: // write to file rt300@18: rt300@0: Json::Value jlogs = logsToJson(); rt300@0: ofFile logFile(fname,ofFile::WriteOnly); rt300@0: logFile << jlogs; rt300@0: logFile.close(); rt300@18: nextLogFileIndex++; rt300@34: theEvents.clear(); rt300@18: } rt300@18: rt300@18: string EventLogger::nextLogFileIndexString(){ rt300@18: string num = ofToString(nextLogFileIndex); rt300@18: string zero = ofToString(0); rt300@18: int numzeros = 4 - num.length(); rt300@18: for (int i=0; i< numzeros;i++){ rt300@18: num = zero + num; rt300@18: } rt300@18: return num; rt300@0: rt300@0: } rt300@0: //---------------------------------------------------------------------------- rt300@0: rt300@0: // this builds up a file incrementally, which can be recovered on crash rt300@0: //void EventLogger::appendToFile(){ rt300@0: // rt300@0: // rt300@0: //} rt300@32: rt300@32: //--------------------------------------------------------------------------- rt300@32: rt300@32: void EventLogger::exitAndSave(){ rt300@32: rt300@32: saveSessionToFile(); rt300@32: return; rt300@32: rt300@32: if(!consentGiven){ rt300@32: Json::Value jlogs = logsToJson(); rt300@32: // try to upload TODO (no - might hang and prevent exit???) rt300@32: rt300@32: // TODO pilot doesn't upload rt300@32: // uploadEventLog(true); rt300@32: return; rt300@32: } rt300@32: rt300@32: savedInteractionTime = savedInteractionTime + (ofGetSystemTime() - sessionStartTime); rt300@32: // save user details rt300@32: string fname = ofxiPhoneGetDocumentsDirectory() + userName + '_' + EVENT_LOG_FILENAME; rt300@32: rt300@32: // try to upload rt300@32: // do it sync because event list needs to be cleared to prevent saving on device rt300@32: rt300@32: // TODO for pilot store to ipad rt300@32: // uploadEventLog(false); rt300@32: rt300@32: // write to file rt300@32: // json without the logs that were uploaded! rt300@32: Json::Value jlogs = logsToJson(); rt300@32: ofFile logFile(fname,ofFile::WriteOnly); rt300@32: logFile << jlogs; rt300@32: cout << jlogs; rt300@32: logFile.close(); rt300@32: rt300@32: }