ICE_PLANT is a remote monitoring and data aquisition device designed to work with the Corometrics 250cx fetal monitoring machine, sending data back to UC Davis LEPS. It functions as a dongle that can be remotely accessed while on an enterprise network.
Download Raspberry Pi imager software and burn a Raspberry Pi OS Lite (64 bit) image to your drive.
When the imager is done, it will automatically eject the sd. Reinsert it and add the usb-gadget.sh and iceplant-gadget.service located in the firmware/ dir in this repository to the bootfs directory. If you are on mac, run this from the root of the repo:
cp firmware/* /Volumes/bootfs/Once completed, you'll have to hook up a screen and keyboard and log into the device to enable a service and edit some files.
Once logged in, run this script to setup the service and serial console login:
sudo chmod +x /boot/firmware/setup.sh
sudo /boot/firmware/setup.shThe service should be enabled but not running. It will start automatically on reboot after you edit some files
Next, open and edit the /boot/firmware/config.txt by adding this line at the bottom:
dtoverlay=dwc2
enable_uart=1Now open the /boot/firmware/cmdline.txt and add this after rootwait with spaces. Be careful to not add new lines here because this is a command:
modules-load=dwc2and to enable serial0 communication for our rs232 adapter, remove from the beginning of the file:
console=serial0,115200Now shut the system down and plug into the computer. Windows users can simply locate the COM in a serial terminal app of their choice.
If you are on mac, you can search for it like this:
ls /dev/cu.* /dev/tty.*You should see something like /dev/cu.usbmodemICEPLANT_00013. It is best to use the cu. version with screen. Using screen, run this command to open a serial shell:
screen /dev/cu.usbmodemICEPLANT_00013 115200or install minicom:
sudo minicom -D /dev/tty.usbmodemICEPLANT_00013 -b 115200Lastly, on you mac find the respective interface (something like en8 or 9 maybe) and run this:
sudo ifconfig en9 inet 192.168.7.1 netmask 255.255.255.0 upNOTE: You may also have to change the order of service in network settings on mac. It might steal your connection and cause your internet connection to fail.
CRITICAL! Turn off you VPN!
On the university network, WPA authentication is used for connecting to WiFi. That means signing on is tricky, and we'll need to provide our own wpa_supplicant-wlan0.conf file. If your system is up and running, then a template for one should have been copied over to the /boot/firmware/ dir. mv it to the correct location and edit the file like this:
sudo cp /boot/firmware/wpa_supplicant-wlan0.conf /etc/wpa_supplicant/
sudo nano /etc/wpa_supplicant-wlan0.confNow you will need to add the SSID (networks name) and your credentials for the network.
There is a cert pointed to at the bottom of the conf that was copied in earlier from the setup.sh script. that contains the keys UC Davis eduroam network, so if another network is going to be used, then a pem will need to be acquired for that network, and the conf should be carefully edited to reflect the method of authentication. The hospital, for example, may not use WPA2 auth as the university does, and the process may be very different.
Once you have correctly configured the wpa_supplicant-wlan0.conf, you can run these commands to enable and start the service:
sudo systemctl enable wpa_supplicant@wlan0
sudo systemctl start wpa_supplicant@wlan0Open raspi-config and got to Localisation options -> L4 WLAN Count... and set your country. Otherwise, rfkill be soft block the wpa supplicant configuration.
If the status shows good on the service running, then run this to ask for an ip on wlan0:
sudo dhclient wlan0
ip addr show wlan0To make sure that dhcp gets an IP for wlan0 automatical create this file:
sudo nano /etc/systemd/network/10-wlan0.networkand add this:
[Match]
Name=wlan0
[Network]
DHCP=yesand run these:
sudo systemctl enable systemd-networkd
sudo systemctl restart systemd-networkd
sudo rebootOnce the board has network connection, run this:
sudo apt update
sudo apt install python3-serial Tailscale is a VPN service that securely connects your devices into a private, peer-to-peer network using WireGuard, without manual firewall or port setup. It allows for remote managing and access of device, even in enterprise networks.
This device is headless, so the clean way to move it to a different Tailscale network is to use an auth key generated from the target tailnet.
Generate a key from the target tailnet from another machine with a browser:
- Sign in to the target Tailscale tailnet
- Open the Keys page
- Generate an auth key with these settings:
- Reusable: ON
- Pre-approved: ON
- Ephemeral: OFF
- Tag:
tag:embedded
Example key format:
tskey-xxxxxxxxxxxxxxxxNow switch networks using these commands using the key you generated in place of the "xxxxxxxxxxxxxxxx":
sudo tailscale logout
sudo tailscale up --auth-key=tskey-xxxxxxxxxxxxxxxx --advertise-tags=tag:embedded
Run these commands to install and enable tailscale:
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable tailscaled
sudo systemctl start tailscaledWhen you are ready to sign into tailscale and add this device to your network, run this command and copy the link it produces into a browser:
sudo tailscale upsudo systemctl stop tailscaled
sudo tailscale logout
sudo rm -rf /var/lib/tailscale/*NOTE: the baud has been found to be 1200. Debug instructions remain as a good example for future debugging of the serial connections.
Currently, the serial to TTL is working on the RPI. The Philips protocol uses 1200 baud, but the Corometrics default is 2400 baud. Use this quick check to see which baud is active:
stty -F /dev/serial0 -a # view current settings
# Try 2400 first (default), then 1200 if nothing useful shows up.
stty -F /dev/serial0 2400 cs8 -cstopb -parenb -crtscts raw -echo
hexdump -C /dev/serial0
stty -F /dev/serial0 1200 cs8 -cstopb -parenb -crtscts raw -echo
hexdump -C /dev/serial0OR if you have minicom installed, you could try:
minicom -D /dev/serial0 -b 2400
minicom -D /dev/serial0 -b 1200The Philips-style framing uses DLE/STX at the start and DLE/ETX before the CRC.
- Capture a short burst at the working baud:
hexdump -C /dev/serial0 | head -n 50- Look for these byte patterns in the output:
- Start of block:
10 02(DLE STX) - End of block:
10 03(DLE ETX), followed by 2 CRC bytes If you see10 02 ... 10 03 xx xxrepeating, the framing matches.
The CTG logger writes hourly CSVs into ICE_PLANT/data/ on the Pi. You can fetch the data manually from a terminal using rsync over SSH as long as you are still in the tailscale network.
Example (run on your laptop):
rsync -avz pi@<pi-hostname-or-ip>:~/ICE_PLANT/data/ ./ # Copies to local directoryThe simplest and best way to get data is by using the dedicated ICE_PLANT_VIEWER application found HERE. That application will allow you to not only download the files, but also to visualize data collection in real-time.
You can generate simulated CTG data on the remote machine to exercise the plotter without hardware for testing and debugging purposes. This is the recommended approach for development of the ICE_PLANT_VIEWER application.
Start the simulator in the background:
python3 simulate_data.py &Tail the newest simulated CSV:
tail -f "$(ls -t data/ctg_frames_sim_*.csv | head -n1)"Simulated data will be labeled with 'sim' in the name, as well as an 'S' instead of the standard 'C' at the start of the payload data (incase we rename the file). NOTE that these sim csv files are not downloaded when using the ICE_PLANT_VIEWER to keep that data clean. If the simulated data is needed, the rsync method can still be used.
ice_plant.py writes CSV logs into data/ and rotates them hourly. The logger writes every CRC-valid payload it sees, regardless of block type (C, S, P, T, I, F, N, etc.). A file is created on startup (header only), and subsequent hourly files are only created when at least one valid frame arrives in that hour.
CSV schema:
timestamp: Unix timestamp when the frame was acceptedblock_type: printable payload type byte (C,S, etc.)payload_len: payload length in bytespayload_hex: raw payload bytes as hex
Interpretation tips:
- Corometrics off or disconnected: no valid frames arrive; the current hour file stays at just the header and no new hourly files appear.
- Corometrics on but no valid signal: valid
Cframes still arrive, but HR/Toco values are often zero/blank. - Corrupted or unexpected serial stream: the service stays up, but if no valid frames are parsed for 5 seconds the logger now clears the buffer and retries the poll/go handshake.
These notes are extracted from doc/Philips_Series_50_-_Programmers_guide.pdf. The Corometrics stream can contain multiple valid payload types; the most common are C for CTG data and S for maternal oxygen saturation. The payload in the CSV always includes the block type byte as its first byte.
Payload layout (byte offsets are within the payload array):
- Byte 0: Block type (
'C'/0x43) - Bytes 1-2: Status (2 bytes)
- Bytes 3-10: HR1 samples (4 x 2 bytes, oldest -> newest)
- Bytes 11-18: HR2 samples (4 x 2 bytes, oldest -> newest)
- Bytes 19-26: MHR samples (4 x 2 bytes, oldest -> newest)
- Bytes 27-30: Toco samples (4 x 1 byte, oldest -> newest)
- Bytes 31-32: HR-Mode (2 bytes)
- Byte 33: Toco-Mode (1 byte)
- Byte 34: FSpO2 value (fetal oxygen saturation; protocol rev dependent)
Field meaning and scaling:
- Heart rate values are 11-bit unsigned with 0.25 bpm resolution (0..1200 => 0..300 bpm; 0 means "blank trace").
- Toco values are 8-bit, 0..127 with 0.5 resolution (stored as 0..255, represented 0..127).
- HR1/HR2/MHR fields include quality and status bits (see Tables 3-6 and 3-7 in the Philips guide).
- Toco and HR modes are encoded bitfields (see Tables 3-9 through 3-11).
Notes:
- HR1/HR2/MHR/Toco are sampled 4 times per second; each C block carries 4 samples.
- The monitor should be polled every 900-1100 ms to avoid missing samples in non-auto mode.
- The maternal finger pulse-ox input is not carried in the
Cblock on this setup. It arrives as anSblock instead.
The S block is used for maternal oxygen saturation, separate from the C block's fetal-SpO2 field.
S payload layout:
- Byte 0: Block type (
'S'/0x53) - Byte 1: Maternal SpO2 at 0.5% resolution (
value / 2.0) - Bytes 2-3: Maternal pulse rate as an 11-bit-style HR word at 0.25 bpm resolution (
value / 4.0, big-endian)
Examples:
53 be 01 54=> SpO2190 / 2 = 95.0%, maternal HR0x0154 / 4 = 85.0 bpm53 b2 02 1c=> SpO2178 / 2 = 89.0%, maternal HR0x021c / 4 = 135.0 bpm
Behavior notes:
Sblocks are intermittent and should be treated as asynchronous updates, not as a fixed once-per-second stream likeC.- The protocol notes used here do not define a fixed
S-block transmit interval. - In downstream tools, the last known maternal SpO2 value should be held until a newer
Sblock arrives or a staleness timeout is reached.
The Philips-style protocol also defines other payload families such as I (identification), P (maternal NIBP), T (maternal temperature), F (failure), N (note), and MM (event message). ICE_PLANT does not filter these out at the logger level; any CRC-valid payload is recorded to CSV.