Skip to content

Commit

Permalink
fix(app-shell): use a wrapping stream for usb (#14214)
Browse files Browse the repository at this point in the history
Implement the tcp socket emulator by creating a separate stream
that is piped through to the usb connection via its downward interface
rather than using the same stream for both the port and the socket or
pipelining the two streams together.

This implementation allows the lifecycles of the USB port connection,
which we want to be the same as the physical robot connection; and the
tcp socket emulators, which we want to be more like tcp sockets; to be
separate, which really increases reliability because we don't have the
port going up and down all the time anymore. This reduces spurious
disconnects.

It will also allow us to add socket activity timeouts to help with the
windows dropping data problem, though we can't really do that until the
http requests that cause synchronous actions that we make get keep alive
streaming responses.
  • Loading branch information
sfoster1 authored Dec 15, 2023
1 parent 9a3065f commit 8c4f1d3
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 176 deletions.
124 changes: 57 additions & 67 deletions app-shell/src/usb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,50 @@ let usbFetchInterval: NodeJS.Timeout
export function getSerialPortHttpAgent(): SerialPortHttpAgent | undefined {
return usbHttpAgent
}
export function createSerialPortHttpAgent(path: string): void {
const serialPortHttpAgent = new SerialPortHttpAgent({
maxFreeSockets: 1,
maxSockets: 1,
maxTotalSockets: 1,
keepAlive: true,
keepAliveMsecs: Infinity,
path,
logger: usbLog,
timeout: 100000,
})
usbHttpAgent = serialPortHttpAgent
export function createSerialPortHttpAgent(
path: string,
onComplete: (err: Error | null, agent?: SerialPortHttpAgent) => void
): void {
if (usbHttpAgent != null) {
onComplete(
new Error('Tried to make a USB http agent when one already existed')
)
} else {
usbHttpAgent = new SerialPortHttpAgent(
{
maxFreeSockets: 1,
maxSockets: 1,
maxTotalSockets: 1,
keepAlive: true,
keepAliveMsecs: Infinity,
path,
logger: usbLog,
timeout: 100000,
},
(err, agent?) => {
if (err != null) {
usbHttpAgent = undefined
}
onComplete(err, agent)
}
)
}
}

export function destroyUsbHttpAgent(): void {
export function destroyAndStopUsbHttpRequests(dispatch: Dispatch): void {
if (usbHttpAgent != null) {
usbHttpAgent.destroy()
}
usbHttpAgent = undefined
ipcMain.removeHandler('usb:request')
dispatch(usbRequestsStop())
// handle any additional invocations of usb:request
ipcMain.handle('usb:request', () =>
Promise.resolve({
status: 400,
statusText: 'USB robot disconnected',
})
)
}

function isUsbDeviceOt3(device: UsbDevice): boolean {
Expand Down Expand Up @@ -115,42 +140,11 @@ function pollSerialPortAndCreateAgent(dispatch: Dispatch): void {
}
usbFetchInterval = setInterval(() => {
// already connected to an Opentrons robot via USB
if (getSerialPortHttpAgent() != null) {
return
}
usbLog.debug('fetching serialport list')
fetchSerialPortList()
.then((list: PortInfo[]) => {
const ot3UsbSerialPort = list.find(
port =>
port.productId?.localeCompare(DEFAULT_PRODUCT_ID, 'en-US', {
sensitivity: 'base',
}) === 0 &&
port.vendorId?.localeCompare(DEFAULT_VENDOR_ID, 'en-US', {
sensitivity: 'base',
}) === 0
)

if (ot3UsbSerialPort == null) {
usbLog.debug('no OT-3 serial port found')
return
}

createSerialPortHttpAgent(ot3UsbSerialPort.path)
// remove any existing handler
ipcMain.removeHandler('usb:request')
ipcMain.handle('usb:request', usbListener)

dispatch(usbRequestsStart())
})
.catch(e =>
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
usbLog.debug(`fetchSerialPortList error ${e?.message ?? 'unknown'}`)
)
tryCreateAndStartUsbHttpRequests(dispatch)
}, 10000)
}

function startUsbHttpRequests(dispatch: Dispatch): void {
function tryCreateAndStartUsbHttpRequests(dispatch: Dispatch): void {
fetchSerialPortList()
.then((list: PortInfo[]) => {
const ot3UsbSerialPort = list.find(
Expand All @@ -165,17 +159,22 @@ function startUsbHttpRequests(dispatch: Dispatch): void {

// retry if no OT-3 serial port found - usb-detection and serialport packages have race condition
if (ot3UsbSerialPort == null) {
usbLog.debug('no OT-3 serial port found, retrying')
setTimeout(() => startUsbHttpRequests(dispatch), 1000)
usbLog.debug('no OT-3 serial port found')
return
}

createSerialPortHttpAgent(ot3UsbSerialPort.path)
// remove any existing handler
ipcMain.removeHandler('usb:request')
ipcMain.handle('usb:request', usbListener)

dispatch(usbRequestsStart())
if (usbHttpAgent == null) {
createSerialPortHttpAgent(ot3UsbSerialPort.path, (err, agent?) => {
if (err != null) {
const message = err?.message ?? err
usbLog.error(`Failed to create serial port: ${message}`)
}
if (agent) {
ipcMain.removeHandler('usb:request')
ipcMain.handle('usb:request', usbListener)
dispatch(usbRequestsStart())
}
})
}
})
.catch(e =>
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Expand All @@ -188,27 +187,18 @@ export function registerUsb(dispatch: Dispatch): (action: Action) => unknown {
switch (action.type) {
case SYSTEM_INFO_INITIALIZED:
if (action.payload.usbDevices.find(isUsbDeviceOt3) != null) {
startUsbHttpRequests(dispatch)
tryCreateAndStartUsbHttpRequests(dispatch)
}
pollSerialPortAndCreateAgent(dispatch)
break
case USB_DEVICE_ADDED:
if (isUsbDeviceOt3(action.payload.usbDevice)) {
startUsbHttpRequests(dispatch)
tryCreateAndStartUsbHttpRequests(dispatch)
}
break
case USB_DEVICE_REMOVED:
if (isUsbDeviceOt3(action.payload.usbDevice)) {
destroyUsbHttpAgent()
ipcMain.removeHandler('usb:request')
dispatch(usbRequestsStop())
// handle any additional invocations of usb:request
ipcMain.handle('usb:request', () =>
Promise.resolve({
status: 400,
statusText: 'USB robot disconnected',
})
)
destroyAndStopUsbHttpRequests(dispatch)
}
break
}
Expand Down
Loading

0 comments on commit 8c4f1d3

Please sign in to comment.