Skip to content

Commit

Permalink
feat: support pushing brainwaves to ipfs & orbis for cn experience
Browse files Browse the repository at this point in the history
  • Loading branch information
oreHGA committed Sep 25, 2024
1 parent b85182d commit b51c89e
Show file tree
Hide file tree
Showing 10 changed files with 3,925 additions and 256 deletions.
3,902 changes: 3,654 additions & 248 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@tanstack/react-query": "^4.36.1",
"@types/archiver": "^5.3.3",
"@types/file-saver": "^2.0.5",
"@useorbis/db-sdk": "^0.0.54-alpha",
"@web3modal/wagmi": "^5.0.11",
"aw-client": "^0.3.7",
"axios": "^1.4.0",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
164 changes: 164 additions & 0 deletions frontend/public/experiments/cn_experience.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<!doctype html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<script src="https://unpkg.com/@jspsych/[email protected]"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/jspsych.css" />
<style>
.jspsych-btn {
margin-bottom: 10px;
}
body {
background-color: #fafafa;
}
#countDown {
position: absolute;
width: 100%;
display: inline;
align-content: center;
justify-content: center;
text-align: center;
}
#countDown p {
align-content: center;
font-weight: 100;
font-size: large;
}
#jspsych-container {
font-size: xx-large;
font-family: sans;
font-weight: bold;
position: absolute;
overflow: none;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="jspsych-container"></div>
<button id="fullscreen-btn" style="position: absolute; top: 10px; right: 10px">Toggle Fullscreen</button>
<script>
document.getElementById("fullscreen-btn").addEventListener("click", function () {
if (!document.fullscreenElement) {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
// Firefox
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
// Chrome, Safari and Opera
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
// IE/Edge
document.documentElement.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
// Firefox
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
// Chrome, Safari and Opera
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
// IE/Edge
document.msExitFullscreen();
}
}
});
</script>
<script>
const jsPsych = initJsPsych({
on_finish: function () {
window.parent.postMessage(jsPsych.data.get(), "*");
},
on_trial_start: function (trial) {
if (!trial.data) {
trial.data = {};
}
trial.data.unixTimestamp = Date.now();
},
display_element: "jspsych-container",
});

const instructions = {
type: jsPsychInstructions,
pages: [
"Welcome to the Causality Neuromarketing Experience!",
"Get Ready: Find a quiet place and put on your headphones.</br> Make sure you're comfortable and ready to focus.",
"Start EEG Recording: If you have your EEG device connected",
],
show_clickable_nav: true,
};

const consent = {
type: jsPsychHtmlButtonResponse,
stimulus:
"Do you understand that by completing this, your brain activity will be recorded and shared with the causality network team for analysis?",
choices: ["Yes", "No"],
button_html: '<button class="jspsych-btn" style="display: inline-block; margin: 0 5px;">%choice%</button>',
on_finish: function (data) {
data.consent = data.response == 0 ? "Yes" : "No";
if (data.response == 1) {
jsPsych.endExperiment("Thank you for your time. The experiment has been terminated.");
}
},
};

const fixation = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "+",
trial_duration: 500,
response_ends_trial: false,
};

const oldBrandImage = {
type: jsPsychImageKeyboardResponse,
stimulus: "./assets/images/cn_experience/old_branding.png",
trial_duration: 800,
response_ends_trial: false,
};

const newBrandImage = {
type: jsPsychImageKeyboardResponse,
stimulus: "./assets/images/cn_experience/new_branding.png",
trial_duration: 800,
response_ends_trial: false,
};

const sequenceGenerator = (trials) => {
const sequence = [fixation, oldBrandImage];
for (let i = 0; i < trials; i++) {
sequence.push(fixation);
sequence.push(Math.random() < 0.7 ? oldBrandImage : newBrandImage);
}
return sequence;
};

// trial sequences
// ref: https://github.com/diamandis-lab/HEROIC/blob/41da0241ea5dd300672a3f3b87268c214d2b14bf/HEROIC-core/session_config/home_session.json
const trials = [];
trials.push(instructions);
trials.push(consent);
trials.push(...sequenceGenerator(20));

trials.push({
type: jsPsychHtmlButtonResponse,
stimulus: "Thank you for your time! We'll download and analyze your data.",
choices: [],
trial_duration: 2000,
});

jsPsych.run(trials);
</script>
</body>
</html>
16 changes: 12 additions & 4 deletions frontend/src/components/lab/experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ export const Experiment: FC<IExperiment> = (experiment) => {
userNpub: session.data?.user?.name,
},
});
await museEEGService.stopRecording(true);
console.log("stopping muse recording");
if (experiment.id === 121) {
await museEEGService.stopRecording(true, false, "ceramic");
} else {
await museEEGService.stopRecording(true, false, "local");
}
}
}

Expand Down Expand Up @@ -333,7 +338,7 @@ export const Experiment: FC<IExperiment> = (experiment) => {
</div>

{/* Neurosity methods */}
{experiment.id !== 6 && (
{![6, 121].includes(experiment.id) && (
<>
<div className="item-start">
{!connectedDevice && (
Expand Down Expand Up @@ -379,8 +384,11 @@ export const Experiment: FC<IExperiment> = (experiment) => {
</div>
</div>
)}

{/* Muse Methods */}
</>
)}
{/* Muse Methods */}
{experiment.id !== 6 && (
<>
<div className="item-start mt-3">
{!museContext?.museClient && (
<Button
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/config/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ export const experiments: IExperiment[] = [
"The Stroop task is a classic test of cognitive control and attentional flexibility. It is often used in clinical and experimental settings to measure selective attention and cognitive control. The task involves naming the color of a word, while ignoring the semantic meaning of the word. For example, the word 'red' might be written in blue ink. The task is often used to measure changes in brain activity associated with attention, relaxation, and other cognitive processes.",
url: "/experiments/stroop_task.html",
},
{
id: 121,
name: "CN Experience",
description:
"This experience is designed by Causality Network to see how people react to a brand change. You will be presented multiple images. Sit still and observe the images.",
url: "/experiments/cn_experience.html",
},
// {
// id: 6,
// name: "Verbal Fluency - Cognitive Test",
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/services/integrations/muse.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dayjs from "dayjs";
import { downloadDataAsZip, getCSVFile, writeToLocalStorage } from "../storage.service";
import { createHash } from "crypto";
import { getFileHash, signData } from "../signer.service";
import { uploadToCeramic } from "../storage.service";

export const MUSE_SAMPLING_RATE = 256;
export const MUSE_CHANNELS = ["TP9", "AF7", "AF8", "TP10"];
Expand Down Expand Up @@ -184,14 +185,32 @@ export class MuseEEGService {
this.museClient.start();
}

async stopRecording(withDownload = false, signDataset = false) {
async stopRecording(
withDownload = false,
signDataset = false,
storageMode: "local" | "remote" | "ceramic" = "local"
) {
this.museClient.pause();
// prepare files for download
const datasetExport: DatasetExport = {
fileNames: [`rawBrainwaves_${this.recordingStartTimestamp}.csv`, `events_${this.recordingStartTimestamp}.csv`],
dataSets: [this.rawBrainwavesParsed, this.eventSeries],
};

console.log("storageMode", storageMode);
if (storageMode === "ceramic") {
console.log("uploading to ceramic");
const brainwavesCSV = await getCSVFile(
`rawBrainwaves_${this.recordingStartTimestamp}.csv`,
this.rawBrainwavesParsed
);
const contentHash = await getFileHash(brainwavesCSV);
console.log("contentHash", contentHash);

const res = await uploadToCeramic(brainwavesCSV);
console.log("res", res);
}

if (signDataset) {
const brainwavesCSV = await getCSVFile(
`rawBrainwaves_${this.recordingStartTimestamp}.csv`,
Expand Down
68 changes: 66 additions & 2 deletions frontend/src/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { DatasetExport } from "~/@types";
import { IDBPDatabase, openDB } from "idb";
import { createHelia } from "helia";
import { unixfs } from "@helia/unixfs";
import { CID } from "multiformats";
import { MemoryBlockstore } from "blockstore-core";

import { OrbisDB, type OrbisConnectResult } from "@useorbis/db-sdk";
import { OrbisKeyDidAuth } from "@useorbis/db-sdk/auth";

export function convertToCSV(arr: any[]) {
const array = [Object.keys(arr[0] ?? {})].concat(arr);
Expand Down Expand Up @@ -143,7 +146,8 @@ export async function writeToLocalStorage(datasetExport: DatasetExport, unixTime
*/
export async function uploadToIpfs(file: File) {
try {
const helia = await createHelia();
const blockstore = new MemoryBlockstore();
const helia = await createHelia({ blockstore });
const fs = unixfs(helia);
const fileBuffer = await file.arrayBuffer();
const cid = await fs.addFile({
Expand All @@ -156,3 +160,63 @@ export async function uploadToIpfs(file: File) {
console.error("Error uploading to IPFS", error);
}
}

export async function uploadToCeramic(file: File) {
try {
// upload to ifps to get cid
const cid = await uploadToIpfs(file);
const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);

const orbis = new OrbisDB({
ceramic: {
gateway: "https://ceramic-orbisdb-mainnet-direct.hirenodes.io/",
},
nodes: [
{
gateway: "https://studio.useorbis.com",
env: process.env["NEXT_PUBLIC_CERAMIC_ENV_ID"],
},
],
});
const auth = await OrbisKeyDidAuth.fromSeed(process.env["NEXT_PUBLIC_CERAMIC_PRIVATE_DID_SEED"] as string);
const authResult: OrbisConnectResult = await orbis.connectUser({ auth });
console.log("authResult", authResult);
// get hash of file
const hashValue = await crypto.subtle.digest("SHA-256", buffer);
const hashArray = Array.from(new Uint8Array(hashValue));
// get bytes32 hash
const hashHex = "0x" + hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
console.log("hashHex", hashHex);

const entry = {
CID: cid ? cid.toString() : "n/a",
name: file.name,
owner: "Causality Network",
contentHash: hashHex,
endTimestamp: new Date().toISOString(),
additionalMeta: "",
startTimestamp: new Date().toISOString(),
};

console.log("entry", entry);

// SAVE TO ORBIS
const updatequery = await orbis
.insert(process.env["NEXT_PUBLIC_CERAMIC_ATTESTATION_TABLE_ID"] as string)
.value(entry)
.context(process.env["NEXT_PUBLIC_CERAMIC_CONTEXT_ID"] as string)
.run();

if (updatequery.content) {
console.log("updatequery.content", updatequery.content);
return true;
}

console.log("updatequery FAILED");
return false;
} catch (error) {
console.log("error", error);
return false;
}
}
2 changes: 1 addition & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
Expand Down

0 comments on commit b51c89e

Please sign in to comment.