Skip to content
Snippets Groups Projects
Jake Read's avatar
Jake Read authored
67f53b9f
History

LTV 1200 Ventilator Display

The LTV-1200 is an (ageing) ventilator, unpopular in practice due to its lack of display.

Luckily, the thing has an RS232 port which streams data in roughly 10ms intervals

We were able to read packets off of this line using a Raspberry Pi, node.js and a small RS232 tranciever circuit from pololu.

video

Using the Vent Display

1: power on the raspberry pi by plugging the micro usb cable in, using i.e. a phone charger. make sure the HDMI cable is connected to a display when you power on.
2: there is a smaller circuit attached to the raspberry pi, that's the RS232 adapter. attached to it is a cable called an "RJ11" - similar to the kind found in corded phone headsets. this should plug into the ltv's "com port" - and the connections between it and the raspberry pi should be made to match the photos below
2: open the 'terminal' application, and run these commands:

  • cd vent/code (navigates the terminal to the code we will run)
  • node ltv (tells node.js to run the ventilator UI server)
    3: the script should output two IP addresses. open a browser (ostensibly anywhere, but this will work best on the raspberry pi itself), and nagivate to either of these IP addresses in the browser. the UI should open, and if the ventilator is running, it will display data.

RS232 -> Raspberry Pi Connections

c1 c2

How it Works

The LTV's serial protocol is well documented by the OEM:

- type electrical baud rate data bits parity stop bits
spec uart R232 60096 8 none 2
permitted " " 57600 8 none 1

The LTV will spew "real time" packets from the RS232 port on a regular 10ms interval, even if you donot ask anything of it - this just happens all the time. This means code does not have to handshake with the device, there is no state in the connection, etc. Data emerges. This is the way.

Packets are simple as well, delineating them is easiest to document with this code snippet (but is also well documented in their serial protocol doc linked above). Structure is like:

b0 b1 b2 b3:n bn
start = 255 length packet type data CRC
let packet = new Uint8Array(255)
let pi = 0	// packet indice (where inside of, during parse)
let pl = 0 	// packet length (expected len)
let ip = false // in packet (delineation state)

parser.on('data', (data) => {
    // read if 
    if(ip){
        if(pi == 0){
            // length byte 
            pl = data[0]
        }
        // store all bytes, increment ptr 
        packet[pi] = data[0]
        pi ++ 
        if(pi > pl + 1){
            onPacket(packet)    // pass, then ptr swap to new... shouldn't leak back 
            packet = new Uint8Array(255) 
            pi = 0 
            pl = 0 
            ip = false 
        }
    }
    // start byte / 0xFA
    if(data[0] == 250 && !ip){
        ip = true 
        pi = 0
    }
})

I only ever read "real time" packets from the device, using this structure:

// switch on the packet type:
switch(pck[1]){
    case 1:
        //realtime data 
        //[2] uint8 insp state (table of 18 states)
        //[3,4] int16 prox pres (-5 to 120)
        //[5,6] int16 xdcr flow (-200 to 200) 
        //[7,8] int16 volume (1ml res) (0 to 3000)
        let data = {
            inspState: TS.read('uint8', pck, 2),
            proxPres: TS.read('int16', pck, 3),
            xdcrFlow: TS.read('int16', pck, 5),
            volume: TS.read('int16', pck, 7)
        }
        // seems like this works, I guess timestamp these things now and keep them around locally...
        // then run a server, client should req. the local store ? 
        store.push([indice, data.inspState, data.proxPres, data.xdcrFlow, data.volume])
        indice ++ 
        // occasionally lop off 1000 entries, so as not to explode local memory 
        if(store.length > 3000){
            console.log("rollover")
            store = store.slice(-2000)
        }
        //console.log(data)
        break;
    default:
        //not plotting others currently 
        break;
}

To pull types from data bytes like this:

TS.read = (type, buffer, start) => {
    switch(type){
        case 'uint8':
            return buffer[start]
        case 'uint16':
            return (buffer[start] & 255 | buffer[start + 1] << 8)
        case 'int16':
            return new Int16Array(buffer.slice(start, start + 2).buffer)[0]
        case 'uint32':
            return (buffer[start] | buffer[start + 1] << 8 | buffer[start + 2] << 16 | buffer[start + 3] << 3)
        default:
            console.log("bad type to read")
            break;
    }
}

Making the Connection

The Raspberry Pi has a hardware serial port, and runs linux. This means we can write our code in javascript (making it easy to update / modify / distribute) while still operating otherwise PITA serialports. Also, javascript is a wonderful language to write servers and clients in.

The serialport needs an RS232 interface, I have also drawn this circuit to connect the raspberry pi's TTL UART to the RS232 connection on the vent:

circuit

The circuit is available in the repository, designed in eagle here.

Server / Client Architecture

To make the web app, I launch ltv.js on the Raspberry Pi which is connected to the LTV. Configuring the Raspberry Pi's serialport can be somewhat cumbersome, I have notes on that in the log.

This initalizes a server on the raspberry pi, and announces its availability on the raspberry pi's terminal. The IP address and port presented can be navigated to with any browser (or phone) and the following will happen:

  • the client will request index.html,
  • the client will load the script referenced in index.html,
    • this script will request that the server start a sub-process: code/local/ltv-bridge.js
  • the ltv-bridge.js process connects to the ltv via the serialport, and to the client via a websocket
    • websocket coordinates are delivered to the client automatically by ltv.js
  • when the ltv-bridge delineates a "realtime" packet on the serialport, it will button this up into a javascript object and whip it across the websocket to the client
  • the client will add this data to a series of plots in the UI and redraw them

What is Not Done

We have not tested this for long durations.

The UI is a scratchpad, and could use work. Recommended settings for the UI were:

  • Horizontal access should be time - 3-6 seconds (width of X axis in plots)
  • Tidal volume is always positive so 0-1000 ml would cover all contingencies.
  • Flow (inspiratory < 100 L/min) and expiratory flow < 150 L/min.
  • Pressure also typically positive, scale is 0 - 80 should cover every possibility - most patients are ventilated at less than 45 cm H2O.

Contact

If you are implementing one of these, and would like our input, simply post an issue on this repository.