I would be remiss not to mention the following contributions:
Lena & Caroline, thank you for your help with putting together my video and slide. I am indebted to SYAP.
Gerard, thank you for the professional quality video footage and images
To my team at BCH: thank you for supporting me as I took on the demands of Fab Academy!
To all the Fab Lab BCN instructors - Josep, Julia, and Adai: thank you for all the time and energy you poured into all of us. Your mentorship made this project possible.
To all of my Fab Academy BCN classmates: you enriched my Fab Academy experience in innumerable ways. Thank you, thank you.
Links
To see the nitty gritty of the development process, see the Project Development page stored alongside my weekly assignments.
To see further detail on the systems integration thought process, design considerations, and steps, see the Systems Integration page.
The header images for all of my weekly assignments were generated with the help of the Microsoft AI Text-to-Image Creator tool.
BOM
Since there are two distinct assemblies in my project, I’ve split the fabrication process into two parts. Part A is for the Heart, and Part B is for the Base.
Heart Assembly
Component
#
Source
Price/Unit
resin 3D printed parts (ventricles, atria, LV window, RV window) - 1 each
Main body (ventricles & great vessels), atria, & removable windows
I used an ANYCUBIC resin printer to 3D print the main body (Ventricles & great vessels), atria (where the electronics & battery would be concealed/contained) and two removeable ventricular viewing windows for the heart.
Slicingon print bedremoved from print bed
Material: white resin
I really would suggest printing this on a resin printer if you have access to one, as the quality of the resulting parts and light diffusion of the resin is preferable. Also, the resin is more amenable to some modification with a dremel- this was required to attach the atria component (which housed the electronics) to the main body of the chambers and outflow tracts. It’s a snug fit!
Depending on the size of your resin printer you may have to print in several batches.
After the print completed, I cleaned the parts in alcohol and carefully removed the support material.
Once the resin prints were completed, I superglued the 4 pairs of magnets into place (making sure each pair was properly oriented to attract rather than repell its partner), 2 pairs per removeble ventricular window piece.
The magnets I used are disks with a diameter of 6mm and thickness of 1.7mm.
RV windowLV windowboth removable windows in place
Removeable VSD portion:
I used the Bambu labs to 3D print the removeable VSD portion/patch which has a magnet embedded inside. I made sure to slice with a support setting of ‘support on bed only’ so that internal supports wouldn’t be added in the cavity where my magnet would go. In the proprietary Bambulabs slicer program I made a note of which layer to add the magnet (right before the cavity started to be enclosed on the top side). You can theoretically add a pause at this point with the slicer, but I just watched the print and paused manually to add the magnet.
Note: make sure to GLUE the magnet in place- otherwise it will attach itself to the nozzle and cause the print to fail.
during the 3D print, after gluing the embedded magnet in place
Print material: white TPU (the VSD patch should be made from a slightly flexible material so it can fit and hold snugly into the VSD hole. Theoretically molding and casting could also be a process to fabricate this piece, still embedding the magnet inside, so long as the resulting piece was opaque to hide the magnet embedded within and flexible to give some tolerance for fitting into the VSD opening).
The magnet I used was one we had in the lab. The dimensions are approximately 8mm in diameter by 5mm tall.
If you do not have access to a magnet of the exact dimensions I used, you can modify the VSD patch piece to fit the magnet you do have available. You’ll want to use a magnet that’s larger rather than smaller to ensure it’s strong enough to be detected by the magnet switch sensor through the resin.
Mill board
1/64" endmill: Front traces
switch to 1/32" endmill, rezero z
In cut –> drill holes
outcut
flip board
switch back to 1/64" endmill; rezero z
Back traces
Stuff board
SchematicLayout
Add vias
solder components as usual
Connect battery - positive terminal directly soldered to the batt + pad on the back of the Xiao ESP32C3, and the GND terminal connected to the appropriate leg of a small slide switch mounted on the board.
Connect the Xiao esp32c3
Test the battery
Connect the magnet switch
Programming
heart board with a cable connected for programming
Download the code for the heart board here or copy and paste the code from below into a new Arduino IDE sketch and upload.
#include <esp_now.h>
#include <WiFi.h>
// Define the structure for receiving data
typedef struct struct_message {
int value;
} struct_message;
// Create a struct_message called myData
struct_message myData;
#include <FastLED.h>
// How many leds in your strip?
#define NUM_LEDS1 56
#define NUM_LEDS2 50
// For led chips like Neopixels, which have a data line, ground, and power, you just
// need to define DATA_PIN. For led chipsets that are SPI based (four wires - data, clock,
// ground, and power), like the LPD8806, define both DATA_PIN and CLOCK_PIN
#define DATA_PIN1 D10
#define DATA_PIN2 D2
#define HALL_SENSOR D0
int BPM = 70;
// Define the array of leds
CRGB leds1[NUM_LEDS1];
CRGB leds2[NUM_LEDS2];
void setup() {
Serial.begin(115200);
Serial.println("resetting");
FastLED.addLeds<WS2812, DATA_PIN1, GRB>(leds1, NUM_LEDS1);
FastLED.addLeds<WS2812, DATA_PIN2, GRB>(leds2, NUM_LEDS2);
FastLED.setBrightness(84);
pinMode(HALL_SENSOR, INPUT_PULLUP);
WiFi.mode(WIFI_STA);
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
// Register for a callback function to receive data
esp_now_register_recv_cb(OnDataRecv);
}
void fadeall1() {
for (int i = 0; i < NUM_LEDS1; i++) { leds1[i].nscale8(230); }
}
void fadeall2() {
for (int i = 0; i < NUM_LEDS2; i++) { leds2[i].nscale8(230); }
}
void loop() {
// Pulse rate: 60 pulses per minute (1 pulse per second)
float delayTimeR = (((60.0/BPM)*1000.0) / (NUM_LEDS1)); // Delay time in milliseconds for each LED;
float delayTimeL = (((60.0/BPM)*1000.0) / (NUM_LEDS2)); // Delay time in milliseconds for each LED;
Serial.println(BPM);
Serial.println(BPM);
// First slide the LED in one direction for blue LEDs
for (int i = 0; i < NUM_LEDS1; i++) {
if (digitalRead(HALL_SENSOR) && i>11) {
leds1[i] = CRGB::Purple;
}
else{
leds1[i] = CRGB::Blue;
}
FastLED.show();
fadeall1();
fadeall2();
delay(delayTimeR);
}
//delay(500);
//First slide the LED in one direction for red LEDs
for (int i = 0; i < NUM_LEDS2; i++) {
leds2[i] = CRGB::Red;
FastLED.show();
fadeall2();
fadeall1();
delay(delayTimeL);
}
}
void OnDataRecv(const esp_now_recv_info *info, const uint8_t *incomingData, int len) {
memcpy(&myData, incomingData, sizeof(myData));
Serial.print("Bytes received: ");
Serial.println(len);
Serial.print("Received value: ");
BPM = myData.value;
Serial.println(BPM);
}
Assembly & Integration
NOTE: This part requires at least two sets of delicate hands, a lot of patience, and a little luck. Use a dremel to carefully remove excess resin where needed (in order to get the atria piece to snap onto the ventricle piece; in order to allow the battery to slide through the board cavity so it would sit between the atria and ventricles inside of its own cavity).
Solder two ulta skinny LED strands to the header pin connector. Ensure GND-GND, data-pin, and VCC-VCC are correct. Test that the connection is good and both strands are lighting up.
separating sections of the ribbon cable to make soldering easier
preparing to solder the LEDs to the ribbon cable
Feed the LED strands through the slots in the atria piece, one into the left atrium and one into the right atrium.
gently feeding the led strands into the atria chambers through holes left for that purpose
Connect the ulta skinny LED strands to the board via the header. Test function.
connecting the leds to the board via the ribbon cable connector
Feed the battery through the cavity ahead of the board so it will rest between the atria piece and the ventricles piece.
feeding the battery through and behind the atria so it will be concealed between the atria and ventricles
Being mindful of the wires and connections, carefully snap the atria piece with the board, battery, and led strips into place onto the ventricles piece. Tuck the magnet switch into the cavity behind the VSD, then tuck the battery in behind it.
LEDs
I hot glued two LED stips into place, one on the right side of the heart and one on the left side of the heart. The excess of each strip will stick out from both the aorta and pulmonary artery.
hot gluing the LED strip to the cardiac chambers
After testing both strands were lighting up correctly, I changed the code and trimmed both LED strips to end at the edge of the geometry rather than extending beyond it.
B. Base Fabrication Process
3D print upper component of the base
I 3D printed the upper portion of the base using the Bambu labs FDM printer, with black PLA filament.
I laser cut the bottom of the base from 6mm thick clear acrylic to enclose the electronics but still allow me to access them from underneath the base if need be.
tracing the top component of the base to determine bounds for the bottom layer of acrylic
Mill
1/64" endmill for traces
1/32" endmill for holes, in cuts, and out cut (in that order)
Stuff board
SchematicLayout
connect to Xiao esp32c3 module
connect potentiometer
Programming
Code for the base board is below, or you can download a file here or copy and paste from below into a new sketch in the Arduino IDE:
#include <esp_now.h>
#include <WiFi.h>
#define POT_PIN D0 // Potentiometer connected to pin D0
// Define the structure for sending data
typedef struct struct_message {
int value;
} struct_message;
struct_message myData;
// Define the peer address (MAC address of the receiver)
uint8_t broadcastAddress[] = {0xD4, 0xF9, 0x8D, 0x00, 0xF9, 0x18}; // Replace with the receiver's MAC address
// Callback function that gets called when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("Last Packet Send Status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
// Global variables
int bpm = 60; // Default BPM
unsigned long lastToggleTime = 0;
unsigned long lastReadTime = 0;
const unsigned long readInterval = 500; // Delay for reading the potentiometer (in milliseconds)
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
Serial.println("WiFi mode set to STA");
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
Serial.println("ESP-NOW initialized");
// Register the send callback
esp_now_register_send_cb(OnDataSent);
// Register peer
esp_now_peer_info_t peerInfo;
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
// Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
return;
}
Serial.println("Peer added");
}
void loop() {
// Get the current time
unsigned long currentTime = millis();
// Check if it's time to read the potentiometer and send data
if (currentTime - lastReadTime >= readInterval) {
// Read the analog value from the potentiometer
int analogValue = analogRead(POT_PIN); // Pin D1 corresponds to ADC1 (GPIO1)
// Map the analog value to the range 30 to 180
bpm = map(analogValue, 0, 4095, 30, 180); // Assuming a 12-bit ADC resolution
// Print the values for debugging
Serial.print("Analog Value: ");
Serial.print(analogValue);
Serial.print(" | Mapped Value: ");
Serial.println(bpm);
// Prepare data to send
myData.value = bpm;
// Send message via ESP-NOW
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
if (result == ESP_OK) {
Serial.println("Sent with success");
} else {
Serial.println("Error sending the data");
}
// Update the last read time
lastReadTime = currentTime;
}
}
Assemble & integrate
Fixate the board to the base (hot glue works) so the connector for the cable is accessible via the hole designed for that purpose in the back of the base.
Stick the stem of the potentiometer through the hole in the front of the base and push the dial over it, securing it in place.
Hot glue the bottom acrylic piece in place under the top part of the base, with the electronics packaged inside.
Overall Integration
The two pieces are modular with each other, meaning that I can individually update components or aspects of either the heart or the base and still have them work together. For instance, in the future I could add additional means of user interaction to the base, such a capacitive plates which can be used to highlight specific chambers when touched. Or a piezoelectric sensor which reads the user’s heartbeat and send it to the Heart to update the ‘heartbeat’ in real time to match that of the user. Since they two parts communicate wirelessly, I would just need to update the code in both pieces as needed after changing/adding additional user inputs.
For more information on my systems integration approach, see the Systems Integration page