Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: Update data paths #476

Merged
merged 17 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 76 additions & 63 deletions src/Electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,44 @@ import { app, BrowserWindow, ipcMain, dialog } from "electron";
import log from "electron-log";
import _ from "lodash";

// TODO @RobertGemmaJr: Figure out how to install the dev tools
// import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';

// const { getPort, sendToPort } = require("./serialPort");

// TODO @RobertGemmaJr: Do more testing with the environment variables - are home/clinic being built correctly?
// TODO @brown-ccv #460: Add serialport's MockBinding for the "Continue Anyway": https://serialport.io/docs/guide-testing

// Early exit when installing on Windows: https://www.electronforge.io/config/makers/squirrel.windows#handling-startup-events
if (require("electron-squirrel-startup")) app.quit();

// Initialize the logger for any renderer process
// TODO @brown-ccv #398: Handle logs in app.getPath('logs')
// TODO @brown-ccv #398: Separate log files for each run through
log.initialize({ preload: true });

// TODO: Fix the security policy instead of ignoring
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";

// TODO @brown-ccv #192: Handle data writing to desktop in a utility process
// TODO @brown-ccv #192: Handle video data writing to desktop in a utility process
// TODO @brown-ccv #398: Separate log files for each run through
// TODO @brown-ccv #429: Use app.getPath('temp') for temporary JSON file

/************ GLOBALS ***********/

// These global variables are created by electron-forge
/* global MAIN_WINDOW_VITE_DEV_SERVER_URL */
/* global MAIN_WINDOW_VITE_NAME */

// TODO: Handle version in renderer - pass into jspsych?
// TODO: Just handle the commit id? I think that's probably fine
// TODO: Preload function for passing this data into renderer - pass into jspsych?
// TODO: Handle at runtime in a separate file not postinstall
const GIT_VERSION = JSON.parse(fs.readFileSync(path.resolve(__dirname, "version.json")));

// TODO @brown-ccv #436 : Use app.isPackaged() to determine if running in dev or prod
RobertGemmaJr marked this conversation as resolved.
Show resolved Hide resolved
// const ELECTRON_START_URL = process.env.ELECTRON_START_URL;
const IS_DEV = import.meta.env.DEV;
let CONTINUE_ANYWAY; // Whether to continue the experiment with no hardware connected

let CONFIG; // Honeycomb configuration object
let CONTINUE_ANYWAY; // Whether to continue the experiment with no hardware connected (option is only available in dev mode)

let TEMP_FILE; // Path to the temporary output file
let OUT_PATH; // Path to the final output folder (on the Desktop)
let OUT_FILE; // Name of the final output file
const DATA_DIR = path.resolve(app.getPath("userData")); // Path to the apps data directory
// TODO @brown-ccv: Is there a way to make this configurable without touching code?
const OUT_DIR = path.resolve(app.getPath("desktop"), app.getName()); // Path to the final output folder
let FILE_PATH; // Relative path to the data file.

let CONFIG; // Honeycomb configuration object
let TRIGGER_CODES; // Trigger codes and IDs for the EEG machine
let TRIGGER_PORT; // Port that the EEG machine is talking through

Expand Down Expand Up @@ -104,13 +99,14 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
log.info("Attempting to quit application");
try {
JSON.parse(fs.readFileSync(TEMP_FILE));
JSON.parse(fs.readFileSync(getDataPath()));
} catch (error) {
if (error instanceof TypeError) {
// TEMP_FILE is undefined at this point
// The JSON file has not been created yet
log.warn("Application quit before the participant started the experiment");
} else if (error instanceof SyntaxError) {
// Trials are still being written (i.e. hasn't hit handleOnFinish function)
// NOTE: The error occurs because the file is not a valid JSON document
log.warn("Application quit while the participant was completing the experiment");
} else {
log.error("Electron encountered an error while quitting:");
Expand Down Expand Up @@ -188,46 +184,49 @@ function handlePhotodiodeTrigger(event, code) {
/**
* Receives the trial data and writes it to a temp file in AppData
* The out path/file and writable stream are initialized if isn't yet
* The temp file is written at ~/userData/[appName]/TempData/[studyID]/[participantID]/
* The temp file is written at ~/userData/[appName]/data/[mode]/[studyID]/[participantID]/[start_date].json
* @param {Event} event The Electron renderer event
* @param {Object} data The trial data
*/
// TODO @brown-ccv #397: Handle FILE_PATH creation when user logs in, not here
function handleOnDataUpdate(event, data) {
const { participant_id, study_id, start_date, trial_index } = data;

// Set the output path and file name if they are not set yet
if (!OUT_PATH) {
// The final OUT_FILE will be nested inside subfolders on the Desktop
OUT_PATH = path.resolve(app.getPath("desktop"), app.getName(), study_id, participant_id);
// TODO @brown-ccv #307: ISO 8061 data string? Doesn't include the punctuation
OUT_FILE = `${start_date}.json`.replaceAll(":", "_"); // (":" are replaced to prevent issues with invalid file names);
}
// The data file has not been created yet
if (!FILE_PATH) {
// Build the relative file path to the file
FILE_PATH = path.join(
"data",
import.meta.env.MODE,
study_id,
participant_id,
// TODO @brown-ccv #307: Use ISO 8061 date? Doesn't include the punctuation (here and in Firebase)
`${start_date}.json`.replaceAll(":", "_") // (":" are replaced to prevent issues with invalid file names
);

// Create the temporary folder & file if it hasn't been created
// TODO @brown-ccv #397: Initialize file stream on login, not here
if (!TEMP_FILE) {
// The tempFile is nested inside "TempData" in the user's local app data folder
const tempPath = path.resolve(app.getPath("userData"), "TempData", study_id, participant_id);
fs.mkdirSync(tempPath, { recursive: true });
TEMP_FILE = path.resolve(tempPath, OUT_FILE);

// Write initial bracket
fs.appendFileSync(TEMP_FILE, "{");
log.info("Temporary file created at ", TEMP_FILE);

// Write useful information and the beginning of the trials array
fs.appendFileSync(TEMP_FILE, `"start_time": "${start_date}",`);
fs.appendFileSync(TEMP_FILE, `"git_version": ${JSON.stringify(GIT_VERSION)},`);
fs.appendFileSync(TEMP_FILE, `"trials": [`);
// Create the data file in userData
const dataPath = getDataPath();
fs.mkdirSync(path.dirname(dataPath), { recursive: true });
fs.writeFileSync(dataPath, "");
log.info("Data file created at ", dataPath);

// Write basic data and initialize the trials array
// TODO @RobertGemmaJr: Handle this entirely in jsPsych, needs to match Firebase
fs.appendFileSync(dataPath, "{");
fs.appendFileSync(dataPath, `"start_time": "${start_date}",`);
fs.appendFileSync(dataPath, `"git_version": ${JSON.stringify(GIT_VERSION)},`);
fs.appendFileSync(dataPath, `"trials": [`);
}

// Prepend comma for all trials except first
if (trial_index > 0) fs.appendFileSync(TEMP_FILE, ",");
const dataPath = getDataPath();

// TODO @RobertGemmaJr: Always write "proper" json (read json and append to it). Will need to update "before-quit" logic
RobertGemmaJr marked this conversation as resolved.
Show resolved Hide resolved
// TODO @brown-ccv #397: I can set a constant for the full path once the stream is created elsewhere
// Write trial data
fs.appendFileSync(TEMP_FILE, JSON.stringify(data));
if (trial_index > 0) fs.appendFileSync(dataPath, ","); // Prepend comma if needed
fs.appendFileSync(dataPath, JSON.stringify(data));

log.info(`Trial ${trial_index} successfully written to TempData`);
log.info(`Trial ${trial_index} successfully written`);
}

/**
Expand All @@ -237,47 +236,51 @@ function handleOnDataUpdate(event, data) {
function handleOnFinish() {
log.info("Experiment Finished");

const dataPath = getDataPath();
const outPath = getOutPath();

// Finish writing JSON
fs.appendFileSync(TEMP_FILE, "]}");
log.info("Finished writing experiment data to TempData");
fs.appendFileSync(dataPath, "]}");
log.info(`Finished writing experiment data to ${dataPath}`);

// Move temp file to the output location
const filePath = path.resolve(OUT_PATH, OUT_FILE);
try {
fs.mkdirSync(OUT_PATH, { recursive: true });
fs.copyFileSync(TEMP_FILE, filePath);
log.info("Successfully saved experiment data to ", filePath);
// NEW
RobertGemmaJr marked this conversation as resolved.
Show resolved Hide resolved
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.copyFileSync(dataPath, outPath);
log.info("Successfully saved experiment data to ", outPath);
} catch (e) {
log.error.error("Unable to save file: ", filePath);
log.error.error(e);
log.error("Unable to save file: ", outPath);
log.error(e);
}
app.quit();
}

// Save webm video file
// TODO @brown-ccv #342: Rolling save of webm video, remux to mp4 at the end?
// TODO @brown-ccv #301: Handle video recordings with jsPsych
function handleSaveVideo(event, data) {
// Video file is the same as OUT_FILE except it's mp4, not json
const filePath = path.join(
path.dirname(OUT_FILE),
path.basename(OUT_FILE, path.extname(OUT_FILE)) + ".webm"
const outPath = getOutPath();
const videoFile = path.join(
path.dirname(outPath),
path.basename(outPath, path.extname(outPath)) + ".webm"
);

log.info(filePath);

// Save video file to the desktop
// TODO @brown-ccv #301: The outputted video is broken
try {
// Note the video data is sent to the main process as a base64 string
const videoData = Buffer.from(data.split(",")[1], "base64");

fs.mkdirSync(OUT_PATH, { recursive: true });
// TODO: This should already have been created?
fs.mkdirSync(path.dirname(videoFile), { recursive: true });
// TODO @brown-ccv #342: Convert to mp4 before final save? https://gist.github.com/AVGP/4c2ce4ab3c67760a0f30a9d54544a060
fs.writeFileSync(path.join(OUT_PATH, filePath), videoData);
fs.writeFileSync(videoFile, videoData);
} catch (e) {
log.error.error("Unable to save file: ", filePath);
log.error.error("Unable to save video file: ", videoFile);
log.error.error(e);
}
log.info("Successfully saved video file to ", filePath);
log.info("Successfully saved video file: ", videoFile);
}

/********** HELPERS **********/
Expand Down Expand Up @@ -347,6 +350,16 @@ function createWindow() {
// mainWindow.loadURL(appURL);
}

/** Returns the absolute path to the JSON file stored in userData */
function getDataPath() {
return path.resolve(DATA_DIR, FILE_PATH);
}

/** Returns the absolute path to the outputted JSON file */
function getOutPath() {
return path.resolve(OUT_DIR, FILE_PATH);
}

/** SERIAL PORT SETUP & COMMUNICATION (EVENT MARKER) */

/**
Expand Down
6 changes: 1 addition & 5 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,4 @@ import App from "./App/App.jsx";
*
* This file renders the React application inside the given location (the browser or Electron)
*/
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
1 change: 0 additions & 1 deletion vite.base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const getDefineKeys = (names) => {
VITE_DEV_SERVER_URL: `${NAME}_VITE_DEV_SERVER_URL`,
VITE_NAME: `${NAME}_VITE_NAME`,
};

return { ...acc, [name]: keys };
}, define);
};
Expand Down
5 changes: 1 addition & 4 deletions vite.renderer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import { defineConfig } from "vite";
import { pluginExposeRenderer } from "./vite.base.config.js";

export default defineConfig((env) => {
/** @type {import('vite').ConfigEnv<'renderer'>} */
const forgeEnv = env;
const { root, mode, forgeConfigSelf } = forgeEnv;
const { root, mode, forgeConfigSelf } = env;
const name = forgeConfigSelf.name ?? "";

/** @type {import('vite').UserConfig} */
return {
root,
mode,
Expand Down
Loading