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

fix(app-shell,app): Send labware files and runtime parameters over USB #14994

Merged
merged 9 commits into from
Apr 25, 2024
4 changes: 3 additions & 1 deletion api-client/src/protocols/createProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export function createProtocol(
runTimeParameterValues?: RunTimeParameterCreateData
): ResponsePromise<Protocol> {
const formData = new FormData()
files.forEach(file => formData.append('files', file, file.name))
files.forEach(file => {
formData.append('files', file, file.name)
})
if (protocolKey != null) formData.append('key', protocolKey)
if (runTimeParameterValues != null)
formData.append(
Expand Down
44 changes: 30 additions & 14 deletions app-shell/src/usb.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import axios, { AxiosRequestConfig } from 'axios'
import FormData from 'form-data'
import fs from 'fs'

Check failure on line 4 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'fs' is defined but never used

Check failure on line 4 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'fs' is defined but never used
import path from 'path'

Check failure on line 5 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'path' is defined but never used

Check failure on line 5 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'path' is defined but never used

import {
fetchSerialPortList,
Expand All @@ -12,7 +12,7 @@
} from '@opentrons/usb-bridge/node-client'

import { createLogger } from './log'
import { getProtocolSrcFilePaths } from './protocol-storage'

Check failure on line 15 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'getProtocolSrcFilePaths' is defined but never used

Check failure on line 15 in app-shell/src/usb.ts

View workflow job for this annotation

GitHub Actions / js checks

'getProtocolSrcFilePaths' is defined but never used
import { usbRequestsStart, usbRequestsStop } from './config/actions'
import {
SYSTEM_INFO_INITIALIZED,
Expand Down Expand Up @@ -83,6 +83,34 @@
device.vendorId === parseInt(DEFAULT_VENDOR_ID, 16)
)
}

type StructuredCloneableFormDataEntry =
| {
type: 'string'
name: string
value: string
}
| {
type: 'file'
name: string
value: ArrayBuffer
filename: string
}

type StructuredCloneableFormData = StructuredCloneableFormDataEntry[]
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved

function reconstructFormData(
structuredCloneableFormData: StructuredCloneableFormData
): FormData {
const result = new FormData()
structuredCloneableFormData.forEach(entry => {
entry.type === 'file'
? result.append(entry.name, Buffer.from(entry.value), entry.filename)
: result.append(entry.name, entry.value)
})
return result
}

async function usbListener(
_event: IpcMainInvokeEvent,
config: AxiosRequestConfig
Expand All @@ -92,21 +120,9 @@
let formHeaders = {}

// check for formDataProxy
if (data?.formDataProxy != null) {
if (data?.proxiedFormData != null) {
// reconstruct FormData
const formData = new FormData()
const { protocolKey } = data.formDataProxy

const srcFilePaths: string[] = await getProtocolSrcFilePaths(protocolKey)

// create readable stream from file
srcFilePaths.forEach(srcFilePath => {
const readStream = fs.createReadStream(srcFilePath)
formData.append('files', readStream, path.basename(srcFilePath))
})

formData.append('key', protocolKey)

const formData = reconstructFormData(data.proxiedFormData)
formHeaders = formData.getHeaders()
data = formData
}
Expand Down
49 changes: 43 additions & 6 deletions app/src/redux/shell/remote.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// access main process remote modules via attachments to `global`
import type { AxiosRequestConfig } from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import type { ResponsePromise } from '@opentrons/api-client'

Check failure on line 3 in app/src/redux/shell/remote.ts

View workflow job for this annotation

GitHub Actions / js checks

'ResponsePromise' is defined but never used

Check failure on line 3 in app/src/redux/shell/remote.ts

View workflow job for this annotation

GitHub Actions / js checks

'ResponsePromise' is defined but never used
import type { Remote, NotifyTopic, NotifyResponseData } from './types'

const emptyRemote: Remote = {} as any
Expand All @@ -20,18 +20,55 @@
},
})

export function appShellRequestor<Data>(
type StructuredCloneableFormDataEntry =
| {
type: 'string'
name: string
value: string
}
| {
type: 'file'
name: string
value: ArrayBuffer
filename: string
}

type StructuredCloneableFormData = StructuredCloneableFormDataEntry[]

async function proxyFormData(
formData: FormData
): Promise<StructuredCloneableFormData> {
const result: StructuredCloneableFormData = []
for (const [name, value] of formData.entries()) {
if (value instanceof File) {
result.push({
type: 'file',
name,
value: await value.arrayBuffer(),
filename: value.name,
})
} else {
result.push({ type: 'string', name, value })
}
}

return result
}

export async function appShellRequestor<Data>(
config: AxiosRequestConfig
): ResponsePromise<Data> {
): Promise<AxiosResponse<Data>> {
const { data } = config
// special case: protocol files and form data cannot be sent through invoke. proxy by protocolKey and handle in app-shell
// Special case: FormData objects can't be sent through invoke().
// Convert it to a structured-cloneable object so it can be.
// app-shell will convert it back.
const formDataProxy =
data instanceof FormData
? { formDataProxy: { protocolKey: data.get('key') } }
? { proxiedFormData: await proxyFormData(data) }
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
: data
const configProxy = { ...config, data: formDataProxy }

return remote.ipcRenderer.invoke('usb:request', configProxy)
return await remote.ipcRenderer.invoke('usb:request', configProxy)
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
}

interface CallbackStore {
Expand Down
Loading