diff --git a/scenarios/scripts/p300-tactile-accumulator.lua b/scenarios/scripts/p300-tactile-accumulator.lua new file mode 100644 index 0000000..ac8240d --- /dev/null +++ b/scenarios/scripts/p300-tactile-accumulator.lua @@ -0,0 +1,200 @@ + +function arrayMax(a) + if #a == 0 then return nil, nil end + local maxIdx, maxValue = 0, a[0] + for i = 1, (#a -1 ) do + if maxValue < a[i] then + maxIdx, maxValue = i, a[i] + end + end + return maxIdx, maxValue +end + +-- For handling target fifo + +List = {} +function List.new () + return {first = 0, last = -1} +end + +function List.pushright (list, value) + local last = list.last + 1 + list.last = last + list[last] = value +end + +function List.popleft (list) + local first = list.first + if first > list.last then + error("list is empty") + end + local value = list[first] + list[first] = nil -- to allow garbage collection + list.first = first + 1 + return value +end + +function List.isempty (list) + if list.first > list.last then + return true + else + return false + end +end + +-- this function is called when the box is initialized +function initialize(box) + + dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua") + + row_base = _G[box:get_setting(2)] + col_base = _G[box:get_setting(3)] + segment_start = _G[box:get_setting(4)] + segment_stop = _G[box:get_setting(5)] + + -- 0 inactive, 1 segment started, 2 segment stopped (can vote) + segment_status = 0 + + -- the idea is to push the flash states to the fifo, and when predictions arrive (with some delay), they are matched in oldest-first fashion. + target_fifo = List.new() + + -- box:log("Info", string.format("pop %d %d", id[1], id[2])) + + row_votes = {} + col_votes = {} + + do_debug = false + +end + +-- this function is called when the box is uninitialized +function uninitialize(box) + +end + +function process(box) + -- loops until box is stopped + while box:keep_processing() do + + -- first, parse the timeline stream + for stimulation = 1, box:get_stimulation_count(2) do + -- gets the received stimulation + local identifier, date, duration = box:get_stimulation(2, 1) + -- discards it + box:remove_stimulation(2, 1) + + if identifier == segment_start then + if do_debug then + box:log("Info", string.format("Trial start")) + box:log("Info", string.format("Clear votes")) + end + -- zero the votes + col_votes = {} + row_votes = {} + target_fifo = List.new() + -- fixme fixed 20 + for i = 0,20 do + col_votes[i] = 0 + row_votes[i] = 0 + end + segment_status = 1 + end + + -- Does the identifier code a flash? if so, put into fifo + if segment_status == 1 and identifier >= row_base and identifier <= OVTK_StimulationId_LabelEnd then + + -- assume rows before cols + if identifier < col_base then + local t = {"row", identifier - row_base} + List.pushright(target_fifo,t) + if do_debug then + box:log("Info", string.format("Push row target %d", identifier - row_base )) + end + else + local t = {"col", identifier - col_base} + List.pushright(target_fifo,t) + if do_debug then + box:log("Info", string.format("Push col target %d", identifier - col_base )) + end + end + + + end + + if identifier == segment_stop then + if do_debug then + box:log("Info", string.format("Trial stop")) + end + segment_status = 2 + end + + end + + -- then parse the classifications + for stimulation = 1, box:get_stimulation_count(1) do + + -- gets the received stimulation + local identifier, date, duration = box:get_stimulation(1, 1) + -- discards it + box:remove_stimulation(1, 1) + + -- Is it an in-class prediction? + if identifier == OVTK_StimulationId_Target then + local t = List.popleft(target_fifo) + if do_debug then + box:log("Info", string.format("Pred fifo %s %d is target", t[1], t[2])) + end + if t[1]=="row" then + row_votes[t[2]] = row_votes[t[2]] + 1 + else + col_votes[t[2]] = col_votes[t[2]] + 1 + end + end + + if identifier == OVTK_StimulationId_NonTarget then + local t = List.popleft(target_fifo) + if do_debug then + box:log("Info", string.format("Pred fifo %s %d is nontarget", t[1], t[2])) + end + end + + end + + if segment_status == 2 and List.isempty(target_fifo) then + -- output the vote after the segment end when we've matched all predictions + + local maxRowIdx, maxRowValue = arrayMax(row_votes) + local maxColIdx, maxColValue = arrayMax(col_votes) + + if maxRowValue == 0 and maxColValue == 0 then + box:log("Warning", string.format("Classifier predicted 'no p300' for all flashes of the trial")); + end + + if do_debug then + local rowVotes = 0 + local colVotes = 0 + for ir, val in pairs(row_votes) do + rowVotes = rowVotes + val + end + for ir, val in pairs(col_votes) do + colVotes = colVotes + val + end + + box:log("Info", string.format("Vote [%d %d] wt [%d,%d]", maxRowIdx+row_base, maxColIdx+col_base, maxRowValue, maxColValue)) + box:log("Info", string.format(" Total [%d %d]", rowVotes, colVotes)) + end + + + + local now = box:get_current_time() + + box:send_stimulation(1, maxRowIdx + row_base, now, 0) + box:send_stimulation(2, maxColIdx + col_base, now, 0) + + segment_status = 0 + end + + box:sleep() + end +end + diff --git a/scenarios/scripts/p300-tactile-filter-flash.lua b/scenarios/scripts/p300-tactile-filter-flash.lua new file mode 100644 index 0000000..9f13ff0 --- /dev/null +++ b/scenarios/scripts/p300-tactile-filter-flash.lua @@ -0,0 +1,61 @@ + +-- Picks out 'flashes' from a stimulation stream + +-- this function is called when the box is initialized +function initialize(box) + + dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua") + + box:set_filter_mode(1); + + state = 0 + + do_debug = false +end + +-- this function is called when the box is uninitialized +function uninitialize(box) +end + +-- this function is called once by the box +function process(box) + + -- loop until box:keep_processing() returns zero + -- cpu will be released with a call to sleep + -- at the end of the loop + while box:keep_processing() do + + -- gets current simulated time + t = box:get_current_time() + + -- loops on every received stimulation for a given input + for stimulation = 1, box:get_stimulation_count(1) do + + -- gets stimulation + stimulation_id, stimulation_time, stimulation_duration = box:get_stimulation(1, 1) + + if stimulation_id == OVTK_StimulationId_SegmentStart then + state = 1 + elseif stimulation_id == OVTK_StimulationId_SegmentStop then + state = 0 + end + + -- If we're between 'rest start' and 'rest_stop', this specifies a target + if state == 1 and stimulation_id >= OVTK_StimulationId_LabelStart and stimulation_id <= OVTK_StimulationId_LabelEnd then + + box:send_stimulation(1, stimulation_id, stimulation_time, 0) + + if do_debug then + box:log("Info", string.format("Push a target %d at %f (now %f)", stimulation_id, stimulation_time, t)) + end + end + + -- discards it + box:remove_stimulation(1, 1) + + end + + -- releases cpu + box:sleep() + end +end diff --git a/scenarios/scripts/p300-tactile-filter-target.lua b/scenarios/scripts/p300-tactile-filter-target.lua new file mode 100644 index 0000000..f8c92db --- /dev/null +++ b/scenarios/scripts/p300-tactile-filter-target.lua @@ -0,0 +1,61 @@ + +-- Picks out 'targets' from a stimulation stream + +-- this function is called when the box is initialized +function initialize(box) + + dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua") + + state = 0 + + box:set_filter_mode(1); + + do_debug = false +end + +-- this function is called when the box is uninitialized +function uninitialize(box) +end + +-- this function is called once by the box +function process(box) + + -- loop until box:keep_processing() returns zero + -- cpu will be released with a call to sleep + -- at the end of the loop + while box:keep_processing() do + + -- gets current simulated time + t = box:get_current_time() + + -- loops on every received stimulation for a given input + for stimulation = 1, box:get_stimulation_count(1) do + + -- gets stimulation + stimulation_id, stimulation_time, stimulation_duration = box:get_stimulation(1, 1) + + if stimulation_id == OVTK_StimulationId_RestStart then + state = 1 + elseif stimulation_id == OVTK_StimulationId_RestStop then + state = 0 + end + + -- If we're between 'rest start' and 'rest_stop', this specifies a target + if state == 1 and stimulation_id >= OVTK_StimulationId_LabelStart and stimulation_id <= OVTK_StimulationId_LabelEnd then + + box:send_stimulation(1, stimulation_id, stimulation_time, 0) + + if do_debug then + box:log("Info", string.format("Push a target %d at %f (now = %f)", stimulation_id, stimulation_time, t)) + end + end + + -- discards it + box:remove_stimulation(1, 1) + + end + + -- releases cpu + box:sleep() + end +end diff --git a/scenarios/scripts/p300-tactile-launch.lua b/scenarios/scripts/p300-tactile-launch.lua new file mode 100644 index 0000000..48f728f --- /dev/null +++ b/scenarios/scripts/p300-tactile-launch.lua @@ -0,0 +1,21 @@ + +-- this function is called when the box is initialized +function initialize(box) + + dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua") + + stim = _G[box:get_setting(2)] + launchTime = box:get_setting(3) + +end + +-- this function is called when the box is uninitialized +function uninitialize(box) +end + +-- this function is called once by the box +function process(box) + + box:send_stimulation(1, stim, launchTime, 0) + +end diff --git a/scenarios/scripts/p300-tactile-stimulator.lua b/scenarios/scripts/p300-tactile-stimulator.lua new file mode 100644 index 0000000..2796393 --- /dev/null +++ b/scenarios/scripts/p300-tactile-stimulator.lua @@ -0,0 +1,138 @@ +-- This Lua script sends Stimulations to the speller visualization box for a tactile p300-System with 6 stimulators +-- +-- Author : Tobias Baumann +-- Date : 2021-12-06 +-- Revised: 2021-19-11 + +--This function lets the box sleep until a fixed moment +function wait_until(box, time) + while box:get_current_time() < time do + box:sleep() + end +end + +--This function lets the box wait for a fixed duration +function wait_for(box, duration) + wait_until(box, box:get_current_time() + duration) +end + +--this function checks, wether value already is an element of the given stim_matrix +function is_element(matrix, value) + for i = 1, #matrix do + if #matrix == 0 then + return(false) + elseif value == matrix[i] then + return(true) + end + end + return(false) +end +--this function creates a sequence of stimulations by shuffeling the values of the given stim_matrix +function create_sequence(matrix) + math.randomseed(os.time()) + local stim_matrix = {} + local stim_code = 0 + local i = 1 + while i <= #matrix do + stim_code = matrix[math.random(1,#matrix)] + if is_element(stim_matrix, stim_code) == false then + stim_matrix[i] = stim_code + i = i + 1 + end + end + return(stim_matrix) +end + +-- this function is called when the box is initialized +function initialize(box) + --load stimulation codes + dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua") + + --load box settings + row_base = _G[box:get_setting(2)] + col_base = _G[box:get_setting(3)] + n_rows = box:get_setting(4) + n_repetitions = box:get_setting(5) + flash_duration = box:get_setting(7) + noflash_duration = box:get_setting(8) + inter_repetition_delay = box:get_setting(9) + inter_trial_delay = box:get_setting(10) + send_toggle = _G[box:get_setting(11)] + time_to_send = box:get_setting(13) + + if box:get_setting(12) == 'true' then + free_spelling = true + n_trials = 1 + else + free_spelling = false + n_trials = box:get_setting(6) + end + + --Lua variables + send = false + experiment_end = false + tactilo_stimcodes = {} + for x = 1, n_rows do + tactilo_stimcodes[x] = row_base + x - 1 + end +end + +-- this function is called when the box is uninitialized +function uninitialize(box) + +end + +-- this function is called once by the box +function process(box) + while box:keep_processing() do + + if send and not experiment_end then + -- iterate over the number of trials + for trial = 1, n_trials do + box:send_stimulation(1, OVTK_StimulationId_RestStart, box:get_current_time() , 0) + wait_for(box, inter_trial_delay) + box:send_stimulation(1, OVTK_StimulationId_RestStop, box:get_current_time() , 0) + box:send_stimulation(1, OVTK_StimulationId_TrialStart ,box:get_current_time() , 0) + + -- iterate over the number of repetitions + for segment = 1, n_repetitions do + tactilo_stimcodes = create_sequence(tactilo_stimcodes) + box:send_stimulation(1, OVTK_StimulationId_SegmentStart ,box:get_current_time() , 0) + + -- iterate over the number of tactilos + for i = 1, #tactilo_stimcodes do + box:send_stimulation(1, tactilo_stimcodes[i] ,box:get_current_time() , 0) + box:send_stimulation(1, OVTK_StimulationId_VisualStimulationStart ,box:get_current_time() , 0) + wait_for(box, flash_duration) + box:send_stimulation(1, OVTK_StimulationId_VisualStimulationStop ,box:get_current_time() , 0) + wait_for(box, noflash_duration) + end + + box:send_stimulation(1, OVTK_StimulationId_SegmentStop ,box:get_current_time() , 0) + wait_for(box, inter_repetition_delay) + end + + box:send_stimulation(1, OVTK_StimulationId_TrialStop ,box:get_current_time() , 0) + + end + + if not free_spelling then + -- end experiment if set to copy spelling + box:send_stimulation(1, OVTK_StimulationId_ExperimentStop ,box:get_current_time() , 0) + send = false + experiment_end = true + end + + else if not send and not experiment_end then + -- delay the start of the experiment by t = time_to_send + wait_for(box, time_to_send) + send = true + box:send_stimulation(1, OVTK_StimulationId_ExperimentStart ,box:get_current_time() , 0) + end + + end + -- releases cpu + box:sleep() + end + +end