This prototype can monitor liquid flow used in many forms of treatment in healthcare. It can signal reliable measurements as well as many types of failures. Communication using Bluetooth Low Energy enables many forms of applications, including alerts on smartphones, dashboards and direct documentation in electronic medical records.
A large area in medicine revolves around fluids entering or leaving the body. Infusion therapy deals with all aspects of fluid and medication infusion, including intravenous application. Intensive care patients are usually monitored for a total fluid balance. Certain groups of chronic patients need to maintain this monitoring outside hospitalization. Provided reliable monitoring patients may self-administer infusions using wearable drug delivery devices.
Managing medicament infusions, nurishment via probe and measuring outgoing fluids in hospitals and other healthcare institutions is time consuming for nurses and is often delayed due to failures, such as blockage or disconnected tubing. These failures are in some cases signaled by uncomfortable audible alarms, but often only detected by manual inspection in-between patient check-ups. Constant exposure to alarms usually leads to warning fatigue, where we start filtering them out.
Having spent some time I hospitals I totally identify with the following clip:
As many healthcare professionals are now equipped with smartphones connected to healthcare systems, adding infusion monitoring will allow spending time on more meaningful tasks.
When patients are transported and treated in ambulances, helicopters and planes remote monitoring of infusions allows an extra level of care from dispatchers and recieving intensive care units.
How does it workThis project, sensing liquid flow in healthcare applications, has been on my list for a few years. I finally discovered a sensor that passed all my criteria:
A medical grade liquid flow sensor, Sensirion LD20, allows for non-invasive, low-energy monitoring. The sensor mechanism is physically separated from the liquid and is produced at a cost point suitable for disposable use. Measurement is accomplished utilizing very sensitive temperature sensors in companionship with a weak source of heat. The movement of the slightly heated liquid is sensed and distinguished from the unheated liquid so that direction and magnitude of flow can be calculated. The following illustration explains how the principle of microthermal liquid measurement works.
Failures such as air-in-line, blockage and free-flow can be derived. Air-in-line can signify disconnection of tubing from source medium, blockage can be caused by patient resting on tubing and free-flow will usually mean that there is a broken connection past the point where the sensor is situated and fluid is leaking. The following illustrations describe how air-in-line is detected.
I have built a prototype that allows transmitting sensor measurements in near-realtime via BLE so that healthcare providers may be notified on important events. I have used the Nordic Semiconductor nRF5340 Development Kit to control and read the sensor via I2C. I use the BLE stack to transmit measurements to nRF Connect for Cloud, allowing further routing to mobile apps and electronic medical record systems. The prototype is housed in a 3D-printed enclosure, designed with CAD, printed using a high precision SLA printer.
MockupAs the Nordic Semiconductor nRF5340 Development Kit exposes a large amount of the microcontroller's features in addition to featuring a debugger and other conveniences, it is a bit larger than the development boards I usually use for portable/wearable prototypes. To convey the potential applications of this prototype I have designed a mockup of what I consider a realistic size of an end-product. This would house a custom PCB for the nRF5340 with a regulator and extended antenna, as well as a coin cell battery. It would accommodate switching disposable sensors, feature some indication of operation and be suitable for sterilisation.
I would really appreciate a series of devboards from Nordic where size and features have been stripped to a minimum. The ON Semiconductor RSL10-COIN is a good example, possibly leaving the added sensors. This would serve as an excellent stage 2 prototype for those of us that aren't quite comfortable with PCB-design and assembly of tiny components.
It so happened that I was able to bring both my functional prototype and mockup to the ambulance services of my local hospital for a photo shoot and to discuss usefullness and practicalities. I got a lot of insightful perspectives and encuragment to take the prototype to the next stage. My sincere appreciation goes to Rune, Thomas and the rest of the crew at Ambulansetjenesten Bodø at Nordlandssykehuset.
A big thanks to Linda at Dyrlegene Bodø for sponsoring the project with a big bag of infusion fluid, this was really important for testing the device.
The liquid flow sensor is available as an evaluation kit (SEK-LD20-2600B). It comes with 3 sensors, each with a practical stand and break-out board with pogo pins. I also opted for the extra USB sensor cable (SCC1-USB), and I'm glad I did, as you will see later on.
The sensor comes in two variants, LD-2600B and LD20-0600L, the difference being the tube connectors.
The sensor contains a small CMOS chip and can be interfaced digitally via I2C. It can reliably measure flow from a few hundred ul/h up to 1300 ml/h. It requires accepts a supply voltage between 3.2V and 3.8V and is rated to consume the following typical amounts of current:
- Measurement: 4.5 mA
- Idle: 0.05 mA
- Sleep mode: 0.001 mA
The USB-connector and software was a great tool for verifying expected readings. It also demonstrates how flow readings can be accumulated to a total.
I started out following the guides to install the toolchain. I did use the Segger Embedded Studio IDE initially, but as I was running through numerous code samples I fell into the habit of using Visual Studio Code. I do miss a debugger and easy source code navigation (for the APIs) so I will probably return. For building and flashing west has worked beautifully. My only confusion is when you have to perform a clean build with --pristine, I probably have ended up doing it more often than needed.
I2C/TWII have never written I2C-driver code from scratch, only modified existing source code. I quickly realized I had not fully understood how a master sends write headers and writes commands. I will not make an attempt of writing an in-depth tutorial, instead I will provide the documentation that I ended up relying on.
Firstly I spent a lot of time studying Zephyr periferal code examples. There was a bit of confusion between the differences in other Nordic boards, and I was struggling to be able to compile and run examples. Later on a Nordic webinar would sum up the approach I ended up using and I urge you to watch it.
The webinar touches on some confusion I picked up while searching for documentation: I2C and TWI (Two-Wire Interface) are referenced interchangeably in this context. Zephyr seems to stick to I2C, while the Nordic documentation references it as TWI, mostly..
In any event, this is the configuration I ended up with in my prj.conf:
CONFIG_I2C=y
CONFIG_I2C_NRFX=y
CONFIG_I2C_1=y
CONFIG_NRFX_TWIM=y
CONFIG_NRFX_TWIM1=y
I created an isolated application to focus on device interface, you can find it in the repository as i2c_ld20. In this code you can see that I initially used the Zephyr Periferal API i2c_transfer. This returned an error code, but inspection with the oscilloscope showed that it was working correctly. I later switched to i2c_write, eliminating the error code. I stuck with the default speed of 400 kbps.
I spent a large portion of the project studying the LD20 datasheet and ended up implementing the required data transfer with the Zephyr Periferal API. Sensirion has provided a sample implementing some procedures for Arduino, this helped me handle the datatypes and such. In the spirit of a time constrained hackathon and Minimum Viable Product, I only implemented the following procedures: Soft reset and Start continuous measurement.
static void ld20_reset()
{
int ret;
uint8_t i2c_cmd = LD20_RESET_CMD;
ret = i2c_write(i2c_dev, &i2c_cmd, 1, 0x00);
if (ret) {
printk("Error writing LD20_RESET_CMD to LD20! error code (%d)\n", ret);
return;
} else {
printk("Wrote LD20_RESET_CMD to address 0x00.\n");
}
// Give LD20 time to soft reboot
k_msleep(25);
}
static void ld20_startcontinous()
{
int ret;
uint8_t i2c_cmd[] = {LD20_STARTCONTINOUS_CMD1, LD20_STARTCONTINOUS_CMD2};
ret = i2c_write(i2c_dev, &i2c_cmd[0], 2, LD20_I2C_ADDRESS);
if (ret) {
printk("Error writing LD20_STARTCONTINOUS to LD20! error code (%d)\n", ret);
//return; Ignore error message, works anyway.
} else {
printk("Wrote LD20_STARTCONTINOUS to address 0x08.\n");
}
k_msleep(120);
}
Notice that the reset command is written to periferal address 0x00, while the actual address is 0x08, as used in the other commands. This is noted in the datasheet and important to pay attention to.
The reset command is a one-byte command, while the other commands are two-byte.
Start continuous measurement does what the name implies, averaging if not polled in 100 ms.
uint8_t i2c_data[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
ret = i2c_read(i2c_dev, &i2c_data[0], 9, LD20_I2C_ADDRESS);
Each read gives
- two bytes for flow rate, with a trailing one-byte CRC
- two bytes for temperature, with a trailing one-byte CRC
- a two-byte encoding for status flags, also with a trailing one-byte CRC
For flow rate the first byte is MSB, so the value is constructed in the following manner:
sensor_flow_value = i2c_data[0] << 8;
sensor_flow_value |= i2c_data[1];
This gives a 16-bit (two byte) unscaled, unsigned value, which in turn is transformed by dividing on a factor defined in the datasheet:
signed_flow_value = (int16_t) sensor_flow_value;
scaled_flow_value = ((float) signed_flow_value) / SCALE_FACTOR_FLOW;
Now we have a signed floating point number (decimal number) that will be negative on reverse flow. Temperature, if needed, is calculated in much the same manner, check the source code for details.
The flags are listed in the datasheet and can be isolated this way:
flag_air_in_line = ((i2c_data[7] >> 0) & 0x01);
flag_high_flow = ((i2c_data[7] >> 1) & 0x01);
flag_exp_smooth = ((i2c_data[7] >> 5) & 0x01);
I do suffer from an issue where the flow readings seem to be scaled incorrectly. The other readings, temperature and flags, are correct, but when comparing using the USB-connector and software I see differences where flow reaches max value (26000) at a low flow rate. I suspect this has something to do with the averaging incurring at low polling rates, but attempts at increasing it have not yielded answers. It might be a bug in my code, please comment if you have any ideas.
The error codes I stumbled upon with i2c_transfer underlines the next point: working with analogue and digital signals can be very challenging without the support of an oscilloscope. A logic analyzer is a great tool too, but it only works if you have a readable signal. A scope will help you identify bad signals, which brings me to the next topic..
I2C, pullup resistorsHaving experimented with digital sensors for a few years now I2C communication starts with pullup or pulldown resistors. The datasheet for the sensor and MCU will tell you which it expects. On a pullup you are basically wiring the clock- and data-signal (SCL, SDA) to the positive logical level so that an idle signal will be logical HIGH and the digital square waves will be nice and sharp when recovering from a LOW. Simply wiring to logical HIGH, in the case of the nRF5430 3 Volts, would be too much to break away from, so you need to find a suitable resistor in-between. Unlike what impression you might get on forums, there are no easy answers as to what resistor value to use. The calculation for finding a good value is complex and takes into account many factors: wire-lenght, number of components sharing the bus, resistance and capacitance of all components involved, moon cycle, to name a few. Ok, maybe not the last part. I take the experimental approach, using the oscilloscope to look for nice sharp square waves. The pins used for I2C_1 do support an internal pullup resistance, but I was struggling to find documentation about this I could understand. I suspect the resistor is of a value around 13k Ohm, so it is too high, not providing sufficient correction. On the following scope grabs I am not sure if the internal resistor is enabled, I did not have time to read through the API, and it really doesn't matter.
Notice in the scope grabs without external pullups how the waves are non-square and don't recover before the next logic shift. Had I relied solely on a logic analyzer I would not have been able to visualize this and I might assume no signal was produced. A voltage meter would be too slow to give any meaningful insight.
I ended up using 2k4 Ohm resistors in this prototype but this might not be right if you reproduce the circuit at home. Another important point is that you might have to re-evaluate the resistors after transferring a circuit from breadboard to a more permanent circuit.
Observe as having experimented with different resistor values resulted in a much more sharp wave. Also, the serial decode feature of my scope is able to display the values of a measurement. At this point i2c_transfer was giving me an error in runtime, but I could observe that communication was working.
Lastly I include a scope grab demonstrating a I2C write for resetting the sensor.
The circuit needed for this prototype was dead-simple, I only needed to connect two I2C-wires from the devboard to the sensor, add external pullups and provide ground and VCC.
For some reason Senserion went with the following color coding on the break-out connector:
- SDA - Brown
- GND - White
- VDD - Blue
- SCL - Black
Why stick to conformity, right?
I made a prototype on a breadboard and later transferred this to a blank perfboard. I connected the perfboard via male header pins to the Arduino-compatible female headers, on both sides to provide stability. I made sure to avoid connections on the underside of the perfboard touching male pins on the devboard. I soldered the male header pin strips on the perfboard while they were sat in the recieving headers, as the alignment is slightly off on the devboard.
After soldering all the connections I spent time checking all the connections looking for shorts and bad connections.
I had never developed for BLE and soon realized my intended approach of just extending a code sample would fall short. I started from basics, reading and watching whatever introductions I could find. The book Getting Started with Bluetooth Low Energy was great in explaining the basics, so was the webinars Nordic has posted over the years. Having developed a lot of prototypes based on LoRaWAN and NB-IoT, I came to realize I had underestimated BLE. This project has given me a new tool and I am sure I will be using it whenever appropriate.
I found the API documentation thorough, but you need to know the BLE concepts to make sense of how to put it all together.
I used both the nRF Connect Bluetooth LE sniffer and Wireshark with BLE plugin extensively, falling in favor of the former due to user-friendlyness.
One thing to note is that the NZXT CAM software I use with my CPU watercooler seems to interfere with the nRF52840 dongle. I found similar reports and have to shut the windows service down before use.
I found a simple 3D model for housing the dongle.
One interesting aspect of my project I want to share is when I wanted to transmit liquid flow as a decimal number. I wanted to be able to display the value in nRF Connect for Cloud in a readable form. This brought me down a rabbit hole of how to encode a signed floating point number, and how to handle the BLE stack reversing the byte order. After a lot of reading and experimentation I realized the answer: don't. That is, don't transmit a float, it's a waste of bytes and requires the receiving end to decode it in a specific way. In the end I transmit the value as the sensor does, leaving it up to applications to scale it as specified in the datasheet.
I think I will go back on my decision to split up the flow rate and flags as separate characteristics, I can't see when I would want to only send one or the other. I will also just transmit the flags as the sensor sends them; as two bytes, and unpack them at the receiving end.
One quirk you might notice is that the two characteristics I have defined share a variable to signify indication. This was me taking shortcuts and something that will be changed when implementing more characteristics.
Another quirk is that I didn't find the time to implement properly read requests of my characteristics. Only notification makes sense in the current version, manual reads return a string from one of the samples I used as a starting point.
I did some experimentation whether to use Notify or Indicate and found a good explanation here.
You might notice there is no characteristic for summing up a total of liquid passing the sensor. This is intentional and should be the responsability of a UI application, most likely a mobile app for paramedics or a dashboard for dispatchers. I don't think adding buttons and indicators for this on the device is a good idea.
For transmitting indications I do the following:
ind_params_flow.uuid = &vnd_ld20_flow_uuid.uuid;
ind_params_flow.attr = &vnd_svc.attrs[0];
ind_params_flow.func = indicate_cb;
ind_params_flow.destroy = indicate_destroy;
ind_params_flow.data = &signed_flow_value;
ind_params_flow.len = sizeof(signed_flow_value);
if (bt_gatt_indicate(NULL, &ind_params_flow) == 0) {
indicating = 1U;
}
As bt_gatt_indicate is asynchronous I added a blocking wait between the two characteristics. Don't follow my example, I just haven't had time to fix it. I think a timer with an interrupt should be a better solution, allowing the device and possibly sensor to sleep between updates.
nRF Connect for CloudI found nRF Cloud intuitive to use with my phone as a BLE gateway. I did some experimentation with both the REST-API and MQTT broker, and could retrieve the state of my device just as expected.
I did not find any documentation for constructing dashboards so I might be missing some features. I didn't find a way to make a simple graph of measurements and I didn't figure out how to use custom formatting. Because of this the flow values in my demo are unreadable, but I can verify the bytes that are received.
I have done a lot of power profiling in my previous projects, having a LoRaWAN mailbox sensor lasting on 2x AAA batteries for 3.5 years and counting. I am looking forward to breaking out the nRF-PPK2 and compare it to other tools I use, unfortunately I ran out of time for the hackathon and will have to do it in the continuation.
CAD EnclosureIt would be impossible to test the prototype without any form of protection, so another major part of the project revolved around designing and 3D-printing an enclosure. I initially did some sketches on a Remarkable pad to get a sense of major problems to solve. Here is a selected page with some ideas for fixing the devboard in place. As you can see you don't need to be an artist, everyone can draw lines and circles.
I used Fusion 360 for CAD. I started by finding all the major components of the prototype as 3D models, mostly STEP-files. Time and again modelling the electronics and other components has saved me numerous design iterations by getting physical constraints up-front. In fact, the first 3D-printed parts fit well, I only decided to make new versions for cosmetics.
I have previously written about CAD and SLA-printing for electronics projects here and here, please explore for further tips and tricks.
In Fusion 360 Section analysis is my go-to feature for enclosures, I constantly switch between cross sections for all 3 axis.
I was pretty happy with the subtle Nordic logo on the lid. I found the logo as.svg-file and embedded it on the bottom plane of the lid. Further 3D-effect was accomplished by slightly embossing the text and half of the logo elements, while indenting the other elements.
I did explore embedding the 3D model of the sensor evaluation bed into the enclosure, but I had to abandon it due to time constraints.
STL-files are available in the repo.
SLA 3D-printingThe enclosure could possibly be printed on a FDM-printer on high resolution, but as a SLA-printer has the print bed inverted I designed it with with this in mind. Specifically this meant careful placement of overhangs.
I also designed it to allow for printing directly on the bed, with no need for support. This is not recommended, as it requires firm adhersion to the bed, often leads to cupping and warping on large horizontal parts. You will also often suffer from elephants feet on the parts touching the bed. I have been experimenting with SLA-printing for a while and print directly on the bed when I want to reduce time spent in post-processing. I intentionally added vent holes to reduce cupping and I was fine with a little bit of warping. Elephants feet were removed with a seam scraper/moldline remover after print.
Cupping is an effect of creating a concave cavity facing the resin tank. Try filling a bowl with water, pressing a plastic cup to the bottom and then lifting it up. You will probably feel a bit of resitance, as it seems to stick to the bottom. This effect can warp your print lines, rip your model from the print bed or in worst case tear the bottom of your resin tank!
For the shell I used the versatile Formlabs Grey V4, for the lid I used Durable V2. Durable is nice for lids, as it is semi-transparent and allows for a degree of flexibility when used with friction-fit parts. Grey doesn't require post-curing but I did it anyway. Durable can achieve different stiffness by adjusting cure-time.
To enhance transparency of Durable and similar materials you can spray a thin layer of clear-coat on both sides. In a pinch you can instead rub the surfaces with petroleum jelly, better known as Vaseline.
My print bed has accumulated quite a bit of scratches the year I have used it. It took me a while to learn and master the side-cutter method of print removal. This is only a problem when printing directly on the bed, as support material is disposed anyway. I decided to get a fresh bed, as this is also allows for starting new prints before the previous one is washed in IPA.
The lid on the new bed came out beautiful so I decided to keep the default level of opaqueness.
SLA-printers are beginning to become accessible with budget models from Prusa, Creality, Elegoo and others, but keep in mind the total cost of resin, tanks and IPA. My previous process consisted of FDM-printing, sanding, spray filler, sanding, more filler, sanding and silicon mold making. A mold like this can last for 50 resin copies, if it is treated well.
Next steps- Optimize for battery life
- Implement more characteristics from the sensor (Part name, serial number, temperature)
- Integrate the data to electronic medical record by listening via MQTT-client, mapping to appropriate HL7 FHIR profile and passing on to open DIPS sandbox environment.
- Create custom board around nRF5430
- Test other scenarios, nasogastric intubation, urine catheter etc.
I have been wanting to do this project for a long time and it proved to require a lot more learning than expected. In the end it turned out almost perfect as of my expectations and I am very proud of the results and future aspects. Being allowed to bring the prototype to an ambulance totally made my day. It gave me insight I had not expected at such an early, technical stage.
I wish to thank Hackster for creating these amazing hackathons and being the hub in a growing community. I also wish to thank Nordic for everything involved in the competition; the free dev kit, support, DevZone and for being at the top in IoT tech. And the stickers!
Comments