diff --git a/README.md b/README.md
index 8777b1dd7b579749d770346e12420c1df3c076fa..305bf421fc04f96ea369f98cd0c9c667795662f2 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,138 @@
-# hexcell
+# Microquine: self-replicating microcontroller code
 
+<img src="img/board_transfer.jpg" width=70%></img>
 
+In this project, we explore self-replication of microcontroller code. The code can jump hosts by simply streaming its own bytes on a UART port.
 
- "i hear biomimicry is the most sincere form of flattery."
- - somebody
+We picked an RP2040 microcontroller equipped with Micropython for the following reasons:
+- As an interpreted language, it offers self-reflection at no extra cost
+- The Python interpreter (REPL) can be made available directly on the UART port of the RP2040
+- Sending a `CTRL-C` (=`\x03`) character resets the target microcontroller and gets it ready for code injection, no matter its current state
+
+## Board
+
+The board we built for these experiments is  a xiao RP2040 with a single cell LiPo battery, a piezo buzzer and UART connectors:
+
+<img src="img/board_v1_1.jpg" width=70%></img>
+
+The LiPo battery is mounted in the back, in a 3D printed enclosure. Thanks to a specific charging manager IC, it can be charged directly from the USB connector's 5V.
+
+<img src="img/parts.jpg" width=70%></img>
+
+## Micropython firmware
+
+For the purpose of this project, we use a version of Micropython in which the REPL can talk to the UART port, in addition to the usual USB CDC port.
+
+You can find a `.uf2` build of this firmware [here](./firmware).
+
+You can install Micropython on the board by resetting the xiao RP2040 and dragging the `.uf2` file onto the flash drive that shows up.
+
+To verify that the REPL is available on the RP2040's UART port, you can connect a USB-to-serial adapter directly to it and power the board through its battery alone:
+
+<img src="img/uart.jpg" width=70%></img>
+
+The USB-to-serial adapter should be set to a baudrate of `115200`. If successful, you'll be greeted by the REPL as if you were directly connected to the RP2040 through its native USB port.
+
+## Minimal self-replicating code
+
+```py
+# main.py
+import machine
+import time
+
+# inject
+print(f"\3f=open('main.py', 'wb')\nf.write({open('main.py', 'rb').read()})\nf.close()\nimport machine\nmachine.reset()")
+
+# blink
+p = machine.Pin(25, machine.Pin.OUT)
+p.value(0)
+time.sleep_ms(200)
+p.value(1)
+```
+
+## Song and dance: using the buzzer and neopixel
+
+```py
+# main.py
+import machine
+import time
+import neopixel
+
+# code injection
+with open("main.py", "rb") as f:
+    print("\x03", end="")
+    print("f = open('main.py', 'wb')")
+    print("f.write(")
+    print(f.read())
+    print(")")
+    print("f.close()")
+    print("import machine")
+    print("machine.reset()")
+
+# start of code
+duty = int(0.6*65535)
+pwm0 = machine.PWM(machine.Pin(3),freq=50_000,duty_u16=0)
+
+# from Ride of the Valkyries, Richard Wagner
+note_time_us = 110_000
+notes = [
+    39, 0, 0, 34, 39, 42, 42, 42, 42, 42, 39, 39, 39, 39, 39,
+    42, 0, 0, 39, 42, 46, 46, 46, 46, 46, 42, 42, 42, 42, 42,
+    46, 0, 0, 42, 46, 49, 49, 49, 49, 49, 37, 37, 37, 37, 37,
+    42, 0, 0, 37, 42, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46
+]
+
+# neopixel color
+machine.Pin(11, machine.Pin.OUT).value(1)
+n = neopixel.NeoPixel(machine.Pin(12), 1)
+n[0] = 0, 0, 24
+n.write()
+
+#    C#    Eb       F#    Ab    Bb       C#    Eb       F#    Ab    Bb
+# C4    D4    E4 F4    G4    A4    B4 C5    D5    E5 F5    G5    A5    B5 C6
+# 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
+pitches = [1e5, 27.50000,29.13524,30.86771,32.70320,34.64783,36.70810,38.89087,
+    41.20344,43.65353,46.24930,48.99943,51.91309,55.00000,58.27047,61.73541,
+    65.40639,69.29566,73.41619,77.78175,82.40689,87.30706,92.49861,97.99886,
+    103.8262,110.0000,116.5409,123.4708,130.8128,138.5913,146.8324,155.5635,
+    164.8138,174.6141,184.9972,195.9977,207.6523,220.0000,233.0819,246.9417,
+    261.6256,277.1826,293.6648,311.1270,329.6276,349.2282,369.9944,391.9954,
+    415.3047,440.0000,466.1638,493.8833,523.2511,554.3653,587.3295,622.2540,
+    659.2551,698.4565,739.9888,783.9909,830.6094,880.0000,932.3275,987.7666,
+    1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,
+    1661.219,1760.000,1864.655,1975.533,2093.005,2217.461,2349.318,2489.016,
+    2637.020,2793.826,2959.955,3135.963,3322.438,3520.000,3729.310,3951.066,
+    4186.009]
+
+delays_us = [int(1e6/(2*pitch)) for pitch in pitches]
+
+def play():
+    t = time.ticks_us()
+    for k in range(len(notes)):
+        tend = t+note_time_us
+        if notes[k] == 0:
+            while (t < tend):
+                t = time.ticks_us()
+            continue
+        delay_us = delays_us[notes[k]]
+        while (t < tend):
+            t = time.ticks_us()
+            pwm0.duty_u16(duty)
+            time.sleep_us(delay_us)
+            pwm0.duty_u16(0)
+            time.sleep_us(delay_us)
+
+play()
+
+machine.reset()
+```
 
 ## Files
 
-[OnShape Assembly](https://cad.onshape.com/documents/dbb1f2f6468431d768c0d460/w/ff7db2adb0811f91412017d1/e/b7272632b534832a71510b02) 
+[OnShape Assembly](https://cad.onshape.com/documents/dbb1f2f6468431d768c0d460/w/ff7db2adb0811f91412017d1/e/b7272632b534832a71510b02)
+
+## License
+
+This project is provided under the MIT license.
+
+Quentin Bolsée and Nikhil Lal, 2024.
diff --git a/code/main_bach.py b/code/main_bach.py
new file mode 100644
index 0000000000000000000000000000000000000000..48c583eee39f1416d8fbc51a4aec57e340ad376e
--- /dev/null
+++ b/code/main_bach.py
@@ -0,0 +1,80 @@
+# main.py
+import machine
+import time
+import neopixel
+
+# code injection
+with open("main.py", "rb") as f:
+    print("\x03", end="")
+    print("f = open('main.py', 'wb')")
+    print("f.write(")
+    print(f.read())
+    print(")")
+    print("f.close()")
+    print("import machine")
+    print("machine.reset()")
+
+# start of code
+duty = int(0.6*65535)
+pwm0 = machine.PWM(machine.Pin(3),freq=50_000,duty_u16=0)
+
+# from Invention No. 8, J.S. Bach
+note_time_us = 140_000
+notes = notes = [
+  0,0,45,45,49,49,45,45,52,52,45,45,
+  57,57,56,54,52,54,52,50,49,50,49,47,
+  45,45,49,49,52,52,49,49,57,57,52,52,
+  61,64,62,64,61,64,62,64,61,64,62,64,
+  57,61,59,61,57,61,59,61,57,61,59,61,
+  54,57,56,57,54,57,56,57,54,57,56,57,
+  51,51,47,47,54,54,51,51,57,57,54,54,
+  59,61,59,57,56,57,56,54,52,54,52,50,
+  49,49,54,52,51,52,51,49,47,49,47,45,
+  44,45,44,42,40,40,52,51,52,52,44,44,
+  45,45,52,52,44,44,52,52,42,42,51,51,
+  52,52,52,52,0,0
+]
+
+# neopixel color
+machine.Pin(11, machine.Pin.OUT).value(1)
+n = neopixel.NeoPixel(machine.Pin(12, machine.Pin.OUT), 1)
+n[0] = 0, 10, 0
+n.write()
+
+#    C#    Eb       F#    Ab    Bb       C#    Eb       F#    Ab    Bb
+# C4    D4    E4 F4    G4    A4    B4 C5    D5    E5 F5    G5    A5    B5 C6
+# 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
+pitches = [1e5, 27.50000,29.13524,30.86771,32.70320,34.64783,36.70810,38.89087,
+    41.20344,43.65353,46.24930,48.99943,51.91309,55.00000,58.27047,61.73541,
+    65.40639,69.29566,73.41619,77.78175,82.40689,87.30706,92.49861,97.99886,
+    103.8262,110.0000,116.5409,123.4708,130.8128,138.5913,146.8324,155.5635,
+    164.8138,174.6141,184.9972,195.9977,207.6523,220.0000,233.0819,246.9417,
+    261.6256,277.1826,293.6648,311.1270,329.6276,349.2282,369.9944,391.9954,
+    415.3047,440.0000,466.1638,493.8833,523.2511,554.3653,587.3295,622.2540,
+    659.2551,698.4565,739.9888,783.9909,830.6094,880.0000,932.3275,987.7666,
+    1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,
+    1661.219,1760.000,1864.655,1975.533,2093.005,2217.461,2349.318,2489.016,
+    2637.020,2793.826,2959.955,3135.963,3322.438,3520.000,3729.310,3951.066,
+    4186.009]
+
+delays_us = [int(1e6/(2*pitch)) for pitch in pitches]
+
+def play():
+    t = time.ticks_us()
+    for k in range(len(notes)):
+        tend = t+note_time_us
+        if notes[k] == 0:
+            while (t < tend):
+                t = time.ticks_us()
+            continue
+        delay_us = delays_us[notes[k]]
+        while (t < tend):
+            t = time.ticks_us()
+            pwm0.duty_u16(duty)
+            time.sleep_us(delay_us)
+            pwm0.duty_u16(0)
+            time.sleep_us(delay_us)
+
+play()
+
+machine.reset()
diff --git a/code/main_blink.py b/code/main_blink.py
new file mode 100644
index 0000000000000000000000000000000000000000..b54e4d77f7d9517eb66e5300ad81991307e29bb4
--- /dev/null
+++ b/code/main_blink.py
@@ -0,0 +1,12 @@
+# main.py
+import machine
+import time
+
+# inject
+print(f"\3f=open('main.py', 'wb')\nf.write({open('main.py', 'rb').read()})\nf.close()\nimport machine\nmachine.reset()")
+
+# blink
+p = machine.Pin(25, machine.Pin.OUT)
+p.value(0)
+time.sleep_ms(200)
+p.value(1)
diff --git a/code/main_sos.py b/code/main_sos.py
new file mode 100644
index 0000000000000000000000000000000000000000..1153a0e638f6bf30c81de2f9fa8c3b168e9b675a
--- /dev/null
+++ b/code/main_sos.py
@@ -0,0 +1,74 @@
+# main.py
+import machine
+import time
+import neopixel
+
+# code injection
+with open("main.py", "rb") as f:
+    print("\x03", end="")
+    print("f = open('main.py', 'wb')")
+    print("f.write(")
+    print(f.read())
+    print(")")
+    print("f.close()")
+    print("import machine")
+    print("machine.reset()")
+
+# start of code
+duty = int(0.6*65535)
+pwm0 = machine.PWM(machine.Pin(3),freq=50_000,duty_u16=0)
+
+# morse code S.O.S.
+note_time_us = 80_000
+notes = notes = [
+  50,0,50,0,50,
+  0,0,0,0,0,
+  50,50,50,0,50,50,50,0,50,50,50,
+  0,0,0,0,0,
+  50,0,50,0,50,
+  0,0,0,0,0,
+]
+
+# neopixel color
+machine.Pin(11, machine.Pin.OUT).value(1)
+n = neopixel.NeoPixel(machine.Pin(12, machine.Pin.OUT), 1)
+n[0] = 10, 0, 0
+n.write()
+
+#    C#    Eb       F#    Ab    Bb       C#    Eb       F#    Ab    Bb
+# C4    D4    E4 F4    G4    A4    B4 C5    D5    E5 F5    G5    A5    B5 C6
+# 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
+pitches = [1e5, 27.50000,29.13524,30.86771,32.70320,34.64783,36.70810,38.89087,
+    41.20344,43.65353,46.24930,48.99943,51.91309,55.00000,58.27047,61.73541,
+    65.40639,69.29566,73.41619,77.78175,82.40689,87.30706,92.49861,97.99886,
+    103.8262,110.0000,116.5409,123.4708,130.8128,138.5913,146.8324,155.5635,
+    164.8138,174.6141,184.9972,195.9977,207.6523,220.0000,233.0819,246.9417,
+    261.6256,277.1826,293.6648,311.1270,329.6276,349.2282,369.9944,391.9954,
+    415.3047,440.0000,466.1638,493.8833,523.2511,554.3653,587.3295,622.2540,
+    659.2551,698.4565,739.9888,783.9909,830.6094,880.0000,932.3275,987.7666,
+    1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,
+    1661.219,1760.000,1864.655,1975.533,2093.005,2217.461,2349.318,2489.016,
+    2637.020,2793.826,2959.955,3135.963,3322.438,3520.000,3729.310,3951.066,
+    4186.009]
+
+delays_us = [int(1e6/(2*pitch)) for pitch in pitches]
+
+def play():
+    t = time.ticks_us()
+    for k in range(len(notes)):
+        tend = t+note_time_us
+        if notes[k] == 0:
+            while (t < tend):
+                t = time.ticks_us()
+            continue
+        delay_us = delays_us[notes[k]]
+        while (t < tend):
+            t = time.ticks_us()
+            pwm0.duty_u16(duty)
+            time.sleep_us(delay_us)
+            pwm0.duty_u16(0)
+            time.sleep_us(delay_us)
+
+play()
+
+machine.reset()
diff --git a/code/main_wagner.py b/code/main_wagner.py
new file mode 100644
index 0000000000000000000000000000000000000000..813583e3e468453af1fed72658ac6b3b968b0d24
--- /dev/null
+++ b/code/main_wagner.py
@@ -0,0 +1,72 @@
+# main.py
+import machine
+import time
+import neopixel
+
+# code injection
+with open("main.py", "rb") as f:
+    print("\x03", end="")
+    print("f = open('main.py', 'wb')")
+    print("f.write(")
+    print(f.read())
+    print(")")
+    print("f.close()")
+    print("import machine")
+    print("machine.reset()")
+
+# start of code
+duty = int(0.6*65535)
+pwm0 = machine.PWM(machine.Pin(3),freq=50_000,duty_u16=0)
+
+# from Ride of the Valkyries, Richard Wagner
+note_time_us = 110_000
+notes = [
+    39, 0, 0, 34, 39, 42, 42, 42, 42, 42, 39, 39, 39, 39, 39,
+    42, 0, 0, 39, 42, 46, 46, 46, 46, 46, 42, 42, 42, 42, 42,
+    46, 0, 0, 42, 46, 49, 49, 49, 49, 49, 37, 37, 37, 37, 37,
+    42, 0, 0, 37, 42, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46
+]
+
+# neopixel color
+machine.Pin(11, machine.Pin.OUT).value(1)
+n = neopixel.NeoPixel(machine.Pin(12), 1)
+n[0] = 0, 0, 24
+n.write()
+
+#    C#    Eb       F#    Ab    Bb       C#    Eb       F#    Ab    Bb
+# C4    D4    E4 F4    G4    A4    B4 C5    D5    E5 F5    G5    A5    B5 C6
+# 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
+pitches = [1e5, 27.50000,29.13524,30.86771,32.70320,34.64783,36.70810,38.89087,
+    41.20344,43.65353,46.24930,48.99943,51.91309,55.00000,58.27047,61.73541,
+    65.40639,69.29566,73.41619,77.78175,82.40689,87.30706,92.49861,97.99886,
+    103.8262,110.0000,116.5409,123.4708,130.8128,138.5913,146.8324,155.5635,
+    164.8138,174.6141,184.9972,195.9977,207.6523,220.0000,233.0819,246.9417,
+    261.6256,277.1826,293.6648,311.1270,329.6276,349.2282,369.9944,391.9954,
+    415.3047,440.0000,466.1638,493.8833,523.2511,554.3653,587.3295,622.2540,
+    659.2551,698.4565,739.9888,783.9909,830.6094,880.0000,932.3275,987.7666,
+    1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,
+    1661.219,1760.000,1864.655,1975.533,2093.005,2217.461,2349.318,2489.016,
+    2637.020,2793.826,2959.955,3135.963,3322.438,3520.000,3729.310,3951.066,
+    4186.009]
+
+delays_us = [int(1e6/(2*pitch)) for pitch in pitches]
+
+def play():
+    t = time.ticks_us()
+    for k in range(len(notes)):
+        tend = t+note_time_us
+        if notes[k] == 0:
+            while (t < tend):
+                t = time.ticks_us()
+            continue
+        delay_us = delays_us[notes[k]]
+        while (t < tend):
+            t = time.ticks_us()
+            pwm0.duty_u16(duty)
+            time.sleep_us(delay_us)
+            pwm0.duty_u16(0)
+            time.sleep_us(delay_us)
+
+play()
+
+machine.reset()
diff --git a/doc/PXL_20240404_011325939.jpg b/doc/PXL_20240404_011325939.jpg
deleted file mode 100644
index 9ac5b9dd3233ce920e1fd417851e0f182e4ae52b..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_011325939.jpg and /dev/null differ
diff --git a/doc/PXL_20240404_011335413.jpg b/doc/PXL_20240404_011335413.jpg
deleted file mode 100644
index 8c9c05c6eb5a8f1f0618964878f6fee7e1e8997c..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_011335413.jpg and /dev/null differ
diff --git a/doc/PXL_20240404_011405820.jpg b/doc/PXL_20240404_011405820.jpg
deleted file mode 100644
index 4e934cc885fd72e4020650a74860359539ec1dde..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_011405820.jpg and /dev/null differ
diff --git a/doc/PXL_20240404_012448719.jpg b/doc/PXL_20240404_012448719.jpg
deleted file mode 100644
index b9b7e06eb1cebc4a8e3c5655f5fb64208b225e17..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_012448719.jpg and /dev/null differ
diff --git a/doc/PXL_20240404_013801518.jpg b/doc/PXL_20240404_013801518.jpg
deleted file mode 100644
index 736a59710bc5911a4e87643acf8bdd68c54e90e5..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_013801518.jpg and /dev/null differ
diff --git a/doc/PXL_20240404_013816159.jpg b/doc/PXL_20240404_013816159.jpg
deleted file mode 100644
index 9357c1ea388a8aa960d108579132e4118a92292d..0000000000000000000000000000000000000000
Binary files a/doc/PXL_20240404_013816159.jpg and /dev/null differ
diff --git a/firmware/firmware.uf2 b/firmware/micropython_rp2_uart.uf2
similarity index 100%
rename from firmware/firmware.uf2
rename to firmware/micropython_rp2_uart.uf2
diff --git a/doc/PXL_20240401_050930159.jpg b/img/board_backlight.jpg
similarity index 100%
rename from doc/PXL_20240401_050930159.jpg
rename to img/board_backlight.jpg
diff --git a/doc/PXL_20240403_014834720.jpg b/img/board_milled.jpg
similarity index 100%
rename from doc/PXL_20240403_014834720.jpg
rename to img/board_milled.jpg
diff --git a/img/board_transfer.jpg b/img/board_transfer.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..979ac719fd9958223afd1940d515e39923be97f7
Binary files /dev/null and b/img/board_transfer.jpg differ
diff --git a/img/board_transfer_chain.jpg b/img/board_transfer_chain.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..baa104ebb3131d24136672211194efb23449edef
Binary files /dev/null and b/img/board_transfer_chain.jpg differ
diff --git a/img/board_v1.jpg b/img/board_v1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..fe6410c103a6e286489b7ddbd564a27c60726ddd
Binary files /dev/null and b/img/board_v1.jpg differ
diff --git a/img/board_v1_1.jpg b/img/board_v1_1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..746db929fbf92c79b7a27ef5e2312626d3183208
Binary files /dev/null and b/img/board_v1_1.jpg differ
diff --git a/doc/PXL_20240404_011413203.jpg b/img/board_v1_1_back.jpg
similarity index 100%
rename from doc/PXL_20240404_011413203.jpg
rename to img/board_v1_1_back.jpg
diff --git a/doc/PXL_20240403_014819343.jpg b/img/milled_all.jpg
similarity index 100%
rename from doc/PXL_20240403_014819343.jpg
rename to img/milled_all.jpg
diff --git a/doc/PXL_20240403_013226992.jpg b/img/milled_traces.jpg
similarity index 100%
rename from doc/PXL_20240403_013226992.jpg
rename to img/milled_traces.jpg
diff --git a/doc/milling_interface.png b/img/milling_interface.png
similarity index 100%
rename from doc/milling_interface.png
rename to img/milling_interface.png
diff --git a/img/parts.jpg b/img/parts.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..34097e1ed199ed240ef1cf02067b68c2932b65ab
Binary files /dev/null and b/img/parts.jpg differ
diff --git a/doc/PXL_20240403_011606789.jpg b/img/probing.jpg
similarity index 100%
rename from doc/PXL_20240403_011606789.jpg
rename to img/probing.jpg
diff --git a/doc/probe.png b/img/probing_result.png
similarity index 100%
rename from doc/probe.png
rename to img/probing_result.png
diff --git a/doc/PXL_20240403_032451975.jpg b/img/soldering1.jpg
similarity index 100%
rename from doc/PXL_20240403_032451975.jpg
rename to img/soldering1.jpg
diff --git a/doc/PXL_20240402_024455802.jpg b/img/soldering2.jpg
similarity index 100%
rename from doc/PXL_20240402_024455802.jpg
rename to img/soldering2.jpg
diff --git a/doc/PXL_20240403_022504258.jpg b/img/two_boards_milled.jpg
similarity index 100%
rename from doc/PXL_20240403_022504258.jpg
rename to img/two_boards_milled.jpg
diff --git a/doc/PXL_20240404_013315167.jpg b/img/uart.jpg
similarity index 100%
rename from doc/PXL_20240404_013315167.jpg
rename to img/uart.jpg
diff --git a/calculator/calculator.py b/pcb/calculator/calculator.py
similarity index 100%
rename from calculator/calculator.py
rename to pcb/calculator/calculator.py