Использование xAPI (Tin Can) и CMI5 в имитаторах

    image
    Несмотря на то, что SCORM 2004 еще «держит позиции», пора начинать поддерживать новые стандарты. Сегодня попробуем разобраться с xAPI / TinCab / CMI5. Обязательно протестируем код на официальных сайтах www.SCORM.com и www.adlnet.gov.

    Итак, Tin Can API — это спецификация программ в сфере дистанционного обучения, которая позволяет обучающим системам общаться между собой путём отслеживания и записи учебных занятий всех видов. Информация об учебной деятельности сохраняется в специальную базу — хранилище учебных записей (англ. learning record store, LRS).

    Подробно можно прочесть на books.ifmo.ru/file/pdf/1772.pdf

    2-ая часть данной статьи — https://habr.com/ru/post/508882/

    Особенности Tin Can API:

    Tin Can API — предлагаемая замена спецификации SCORM
    Tin Can API позволяет записывать любой опыт обучения, что дает нам более полную картину обучения конкретного человека
    Tin Can API снимает с данных ограничения, накладываемые СДО
    Tin Can API способен оказать неоценимую помощь учебным отделам, сопоставляя данные о качестве выполнения работы с учебными данными, тем самым повышая эффективность обучения.

    Это теория, теперь практика.

    При работе с SCORM все было относительно просто, нужно было «выставить» значения фиксированных переменных или получить значения фиксированных переменных.

    Ну например…

    min = 0
    max= 100
    raw_score = 100
    scaled = raw_score / max -- Оценка приведенная к диапазону 0..1.
    	
    ScormSetValue("cmi.score.min", ""..min); -- Минимальная оценка
    ScormSetValue("cmi.score.max", ""..max); -- Максимальная оценка
    ScormSetValue("cmi.score.raw", ""..raw_score); -- Полученная оценка
    ScormSetValue("cmi.score.scaled", ""..scaled); -- Оценка приведенная к диапазону 0..1.
    
    --Объем (количество) 0..1
    ScormSetValue("cmi.progress_measure", "1");
    
    ScormSetValue("cmi.success_status", "passed");
    ScormSetValue("cmi.completion_status", "completed");
    
    ScormGetValue("cmi.learner_name");
    ScormGetValue("cmi.learner_id");
    ScormGetValue("cmi.suspend_data");
    ScormGetValue("cmi.scaled_passing_score");
    ScormGetValue("cmi.completion_threshold");
    
    print ( ScormGetValue("cmi._version"))
    print ( ScormGetValue("cmi.total_time"))
    print ( ScormGetValue("cmi.time_limit_action"))
    print ( ScormGetValue("cmi.max_time_allowed"))
    
    --Запись значений интеракций
    ScormSetValue("cmi.interactions.0.id","Step1"); 
    ScormSetValue("cmi.interactions.0.description", "17:14:28	Произвести аварийный останов работающих компрессорных станций")
    ScormSetValue("cmi.interactions.0.result","correct");
    
    ScormSetValue("cmi.interactions.1.id","Step2"); 
    ScormSetValue("cmi.interactions.1.type","fill-in"); 
    ScormSetValue("cmi.interactions.1.objectives.0.id","urn:ADL:objectiveid-0001");
    ScormSetValue("cmi.interactions.1.description", "privet"); 
    ScormSetValue("cmi.interactions.1.learner_response", "privet"); 
    ScormSetValue("cmi.interactions.1.timestamp", "2005-10-11T09:00:30");
    ScormSetValue("cmi.interactions.1.correct_responses.0.pattern", "privet");
    ScormSetValue("cmi.interactions.1.weighting", "1");
    --correct, incorrect, unanticipated, neutral , number 0..1
    ScormSetValue("cmi.interactions.1.result","unanticipated");
    ScormSetValue("cmi.interactions.1.latency", "PT0H0M5.0S");
    
    ScormSetValue ("cmi.comments_from_learner.0.comment",q1);
    ScormSetValue ("cmi.comments_from_learner.1.comment",q2);
    

    Примерно так все и делалось… Теперь на xAPI…

    Далее идет список тех LRS на которых я выполнял тестирование взаимодействия (необходима регистрация и получение login/pass соответственно)…


    Для взаимодействия с xAPI на C++ нам понадобится CURL и какая-нибудь библиотека для работы с JSON (cJSON например)…

    Тогда использование xAPI можно выполнять например так:

    TinCanAddRecord("actor:::mbox:::mailto:mathmodel@mathmodel.com")			TinCanAddRecord("actor:::name:::mathmodel")
    TinCanAddRecord("actor:::objectType:::Agent")
    TinCanAddRecord("verb:::id:::http://adlnet.gov/expapi/verbs/interacted")
    TinCanAddRecord("object:::id:::http://lcontent.ru/lms1/simulator2")
    TinCanAddRecord("object:::objectType:::Activity")
    			
    			TinCanAddRecord("object:::definition:::type:::http://www.lcontent.ru/lms1/simulator1")
    TinCanAddRecord("object:::definition:::name:::en-US:::mathmodel")
    TinCanAddRecord("object:::definition:::description:::en-US:::mathmodel log")
    			
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/Teapot1 angle:::" .. a1)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/Teapot2 angle:::" .. a2)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/Teapot3 angle:::" .. a3)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/time:::" .. (os.clock() - veryoldtime))
    			
    TinCanAddRecord("actor:::mbox:::mailto:maxgammer@gmail.com")
    TinCanAddRecord("actor:::name:::Maxim Gammer")
    TinCanAddRecord("actor:::objectType:::Agent")
    TinCanAddRecord("verb:::id:::http://adlnet.gov/expapi/verbs/interacted")
    TinCanAddRecord("object:::id:::http://lcontent.ru/lms1/simulator2")
    TinCanAddRecord("object:::objectType:::Activity")
    TinCanAddRecord("object:::definition:::type:::http://lcontent.ru/lms1/simulator1")
    TinCanAddRecord("object:::definition:::name:::en-US:::User move")
    TinCanAddRecord("object:::definition:::description:::en-US:::User coordinates")
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/time:::" .. (os.clock() - veryoldtime))
    			
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/X:::" .. UserData.X)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/Y:::" .. UserData.Y)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/Z:::" .. UserData.Z)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/HeadYaw:::" .. UserData.HeadYaw)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/HeadPitch:::" .. UserData.HeadPitch)
    TinCanAddRecord("object:::extensions:::http://lcontent.ru/upsv/HeadRoll:::" .. UserData.HeadRoll)

    Все, смотрим записи в LRS.

    "

    InterfaceForTinCan.cpp
    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <string>
    #include <string.h>
    #include <iostream>
    #include <fstream>
    
    #include <vector>
    #include <map>
    #include <algorithm>
    #include <iterator>
    
    #include <curl/curl.h>
    
    //using namespace std;
    
    #ifdef WIN32
    	#include "./cJSON.h"
    #else
    	#include "./cJSON.h"
    #endif
    
    class InterfaceForTinCan
    {
    public:
    	InterfaceForTinCan();
    	void AddTinCanRecord(std::string str, std::string type);
    	void PostToLRS(std::string host, std::string login, std::string password);
    	void PostFileToLRS(std::string filename);
    	void PostToFile (std::string filename);
    	
    
    private:
    	std::vector<std::string> split(const std::string& s, const std::string& delim, const bool keep_empty = true ) ;
    
    	//созданные объекты
    	std::map <std::string, cJSON *> OBJECTS;
    	//
    	cJSON *top;
    
    	std::string LRS_host;
    	std::string LRS_login;
    	std::string LRS_password;
    
    
    
    	void PostStringToLRS(std::string zzz);
    
    };
    
    
    InterfaceForTinCan::InterfaceForTinCan()
    {
    	top=cJSON_CreateObject();
    }
    
    void InterfaceForTinCan::AddTinCanRecord(std::string str, std::string type)
    {
    	//1. преобразуем страку в вектор через разделитель (::: или @@@)
    	std::vector<std::string> words = split(str, ":::");
    	//последние 2 элемента это поле=значение
    	int numOfObject = words.size();
    
    	//
    	std::string z =  words [0];
    	if( OBJECTS.end() != OBJECTS.find(z))
    	{
    		//ключ присутствует
    	} 
    	else
    	{
    		//создаем
    		OBJECTS[z] =cJSON_CreateObject();
    		//клеим к root
    		cJSON_AddItemToObject(top,z.c_str(), OBJECTS[z]);
    	}
    
    	for (int i=1; i < numOfObject -2; i++)
    	{
    		std::string oldz = z;
    		z = z + ":::" + words [i];
    
    		if( OBJECTS.end() != OBJECTS.find(z))
    		{
    			//ключ присутствует
    		} 
    		else
    		{
    			//создаем
    			OBJECTS[z] =cJSON_CreateObject();
    			//клеим к 
    			cJSON_AddItemToObject(OBJECTS[oldz], words [i].c_str(), OBJECTS[z]);
    		}
    	}
    
    	std::string value = words [numOfObject-1];
    	if (type=="string")
    	{
    		cJSON_AddStringToObject(OBJECTS[z], words [numOfObject-2].c_str(), value.c_str());
    	}
    	else if  (type=="number")
    	{
    		cJSON_AddNumberToObject(OBJECTS[z], words [numOfObject-2].c_str(), std::stod(value));
    	}
    	else if  (type=="bool")
    	{
    		bool val = false;
    		if ((value=="true")||(value=="TRUE")) val = true;
    		cJSON_AddBoolToObject(OBJECTS[z], words [numOfObject-2].c_str(), val);
    	}
    }
    
    
    void InterfaceForTinCan::PostToLRS(std::string host, std::string login, std::string password)
    {
    	char* out=cJSON_Print(top);	
    	std::string zzz = out;
    	cJSON_Delete(top);
    	OBJECTS.clear();
    
    	printf("%s\n",out);
    	free(out);
    	top=cJSON_CreateObject();
    
    
    	LRS_host = host;
    	LRS_login = login;
    	LRS_password = password;
    
    	PostStringToLRS(zzz);
    }
    
    void InterfaceForTinCan::PostFileToLRS(std::string filename)
    {
    	std::string zzz;
    	std::string line;
    	std::ifstream myfile (filename.c_str());
        if (myfile.is_open())
        {
    		while ( myfile.good() )
    		{
    			getline (myfile,line);
    			zzz = zzz + line;
    		}
    		myfile.close();
        }
    	//
    	PostStringToLRS(zzz);
    }
    
    void InterfaceForTinCan::PostToFile(std::string filename)
    {
    	char* out=cJSON_Print(top);	
    	std::string zzz = out;
    	cJSON_Delete(top);
    	OBJECTS.clear();
    	
    	std::ofstream myfile;
    	myfile.open (filename);
    	myfile << zzz;
    	myfile.close();
    
    	free(out);
    	top=cJSON_CreateObject();
    }
    
    void InterfaceForTinCan::PostStringToLRS(std::string zzz)
    {
    	std::string URL = LRS_host; //"https://cloud.scorm.com/ScormEngineInterface/TCAPI/public/statements";
    	std::string loginpassword = LRS_login + ":" + LRS_password; //"test:test" 
    
    	CURL *curl;
    	struct curl_slist *headers=NULL; 
    
        headers = curl_slist_append(headers, "Accept: application/json");
        headers = curl_slist_append( headers, "Content-Type: application/json");
        headers = curl_slist_append( headers, "X-Experience-API-Version:1.0.0");
        headers = curl_slist_append( headers, "charsets: utf-8");
    
    	curl = curl_easy_init(); 
    
        if (curl)
        {
            /* enable verbose for easier tracing */
            curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    
            curl_easy_setopt(curl, CURLOPT_URL, URL.c_str());
            curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); //PUT
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    		//
            curl_easy_setopt( curl, CURLOPT_USERPWD, loginpassword.c_str() ); //"test:test"
    		// With the curl command line tool, you disable this with -k/--insecure.
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);
    		
            curl_easy_setopt(curl, CURLOPT_POST, 1);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, zzz.c_str());
    
            std::cout<< "..." << std::endl;
    		CURLcode res = curl_easy_perform(curl);
            std::cout<<   std::endl << "..." << std::endl;
    
    		/* Check for errors */ 
            if(res != CURLE_OK)
            {
                std::cout<< "error:" << std::endl;
                fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
                std::cout << std::endl;
            }
    
    		curl_easy_cleanup(curl);
    	}
    	else
    	{
    		std::cout << "false" << std::endl;
    	}
    }
    
    std::vector<std::string> InterfaceForTinCan::split(const std::string& s, const std::string& delim, const bool keep_empty) 
    {
    	std::vector <std::string> result;
    	if (delim.empty()) 
    	{
    		result.push_back(s);
    		return result;
    	}
        std::string::const_iterator substart = s.begin(), subend;
        while (true) 
    	{
            subend = search(substart, s.end(), delim.begin(), delim.end());
            std::string temp(substart, subend);
            if (keep_empty || !temp.empty()) {
                result.push_back(temp);
            }
            if (subend == s.end()) {
                break;
            }
            substart = subend + delim.size();
        }
        return result;
    }
    

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 15

      +2
      Давненько меня не припекало что-то прокомментировать в интернетах. А по такому случаю, не поленился даже завести аккаунт.

      Tin Can API — улучшенная версия спецификации SCORM

      Неа. SCORM — это трехтомник. Про упаковку контента, про навигацию в электронных курсах и про то, какая должна быть среда выплонения (тут в частности про сохранение данных о прохождении обучения пользователем). К этому прилагаются сценарии тестирования продуктов, сделанных по скорму. За них спецам из ADL отдельное «спасибо». А xAPI исключительно про то, что данные нужно гонять через REST API, а не через прокладку на фронте LMS.

      Tin Can API снимает с данных ограничения, накладываемые СДО

      За это общем-то и критикуется. Сколькими способами в формате «actor verb object» можно записать, что пользователь завершил обучение? Больше, чем достаточно. В SCORM 2004 4th ed, например, есть cmi.success_status и cmi.completion_status. Двух переменных уже хватает для того, чтобы разработчики LMS, авторских средств и курсов в них запутались, т.к. каждый трактуерт стандарт по-своему. А xAPI дает ещё больше возможостей накосячить.

      Tin Can API способен оказать неоценимую помощь учебным отделам, сопоставляя данные о качестве выполнения работы с учебными данными, тем самым повышая эффективность обучения.

      А кто мешает сопоставлять данные работы с учебными данными без xAPI?

      При работе с SCORM 2000

      Очепятка? Есть SCORM 1.1, 1.2 и 2004 в четырех редакциях.

      Это теория, теперь практика.
      1. Честно, пример со скормом выглядит по-проще, что ли. В чем, в данном случае, преимущество от перехода на xAPI?
      2. Зачем в первом примере записывать взаимодействие «Произвести аварийный останов работающих компрессорных станций» через interactions, которое к тому же fill-in, а не сразу через objectives?
      3. Очень интересно было бы увидеть контекст: зачем всё это, что за иммитатор вы делаете, для какой LMS/LRS/LXP/HCMS (нужное подчеркнуть), почему C, а не JS, в конце концов. Был бы контекст, возможно, первых двух вопросов не возникло бы.

      А вообще, большое спасибо за статью! Мне кажется, хороших материалов про технологии e-Learning в российской части интернета крайне мало, в отличие от шлака про геймификацию иммерсивного микрообучения. Очень интересно узнать об опыте тех, кто сам программирует учебные решения, а не только рисует во всяких там сторилайнах.
        0
        По версии… имеется ввиду «последняя» версия- 2004 4th Edition.

        Tin Can API — улучшенная версия спецификации SCORM. С критикой согласен. Изменю.

        Tin Can API снимает с данных ограничения, накладываемые СДО. А вот это верно, с моей точки зрения. Проблема была огромная, можно было только передавать в рамках сессии в LMS. При запуске «EXE» вообще был кошмар типа передача данных от exe в какой-нибудь JAVA-апплет, JAVA-апплет в JavaScript… жуть. Сейчас нет никаких проблем, передавай и запрашивай что нужно и когда нужно.

        cmi.success_status и cmi.completion_status. Я лично трактую как «сколько заданий выполнено» (количественно) и с какой «точностью», т.е. сколько выполнено и как правильно выполнено.

        А кто мешает сопоставлять данные работы с учебными данными без xAPI?
        Т.е. каждый будет изобретать свой велосипед? Нет, лучше уж так.

        1. Честно, пример со скормом выглядит по-проще, что ли. В чем, в данном случае, преимущество от перехода на xAPI?
        xAPI сильно сложнее, но позволяет передавать все что только может прийти в голову. Мне лично это очень необходимо.

        2. Зачем в первом примере записывать взаимодействие «Произвести аварийный останов работающих компрессорных станций» через interactions, которое к тому же fill-in, а не сразу через objectives?

        Показываю, что можно и так)

        3. Очень интересно было бы увидеть контекст: зачем всё это, что за иммитатор вы делаете, для какой LMS/LRS/LXP/HCMS (нужное подчеркнуть), почему C, а не JS, в конце концов. Был бы контекст, возможно, первых двух вопросов не возникло бы.

        habr.com/ru/post/508604
        habr.com/ru/post/508532
        habr.com/ru/post/508522
        habr.com/ru/post/508478
        habr.com/ru/post/508428
        habr.com/ru/post/508390

        Это если коротко, подробнее у меня на сайте.

          0

          Ещё одно важное обстоятельство.


          На практике я не видел ни одной lms системы, которая полностью могла сохранять и тем более как-то показывать полный набор scorn 2004v4. Даже сейчас тоже Moodle не может этого…
          А что делать если продукт передается например в lms которая только 5-6 параметров сохраняет, а остальные нет?


          Так вот с xApi лучше. По крайней мере я передаю все что хочу, и другая сторона это гарантированно получает. А вот как она это будет обрабатывать — не моя проблема))) вот в этом большая разница между scorm и tincan.

            +1
            Я лично написал такую систему. Прошла все тесты SCORM Compliance и была сертифицирована ADL.
              0
              Интересно…
              Где/кем используется (если не секрет)?
                0
                Сейчас — не знаю, я уже 5 лет этим не занимаюсь. Раньше была интегрирована в сейлсфорс.
            0

            Произвести аварийный останов работающих компрессорных станций» через interactions, которое к тому же fill-in, а не сразу через objectives


            Тут имеется ввиду что это не задача обучаемому, а то что обучаемый сам намерен сделать, т.е. его личное решение.

            0

            Очень хорошо что ушли от стандартизации хранения контента…
            Сейчас вот так даже можно…
            https://m.habr.com/ru/post/325980/

              0
              2я часть статьи — habr.com/ru/post/508882
                0
                Спасибо за ответы! Если бы мог, плюсанул.
                  0
                  Спасибо за грамотные вопросы!
                +1
                3я часть статьи когда будет?
                0
                В этом месяце)

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое