diff --git a/2023-12_usb-real-data-rates.md b/2023-12_usb-real-data-rates.md index c9826ccf562c6fbec0cd87c664451880e284ff4e..bacf107ff2f43608030d0ba72d5dc77cb7cbad9d 100644 --- a/2023-12_usb-real-data-rates.md +++ b/2023-12_usb-real-data-rates.md @@ -32,12 +32,63 @@ This is also not accounting for i.e. multiple devices, flow control, flow going This also doesn't properly inspect whether / not there is significant performance dings due to i.e. cobs, which is [some looping python, anyways](https://github.com/cmcqueen/cobs-python/blob/main/src/cobs/cobs/_cobs_py.py) - so, maybe there is real evidence that we want to i.e. ethernet to the first thing, etc. -## 2023 12 20 +## 2023 12 21 So, we want to... consume and produce data, as fast as possible in either direction, and open multiple ports. I think that the multiple ports thing is going to teach me what I want to know about asyncio, and is simple enough that I can try to replicate it with multiprocessing as well. +... we're into that, we just need to run 'em both and plot two-ups, this should be simple next step, then compare to multiprocessing ! + +## 2023 12 27 + +OK, first let's test multi-sink using blocking codes. I'll modify the original to not "read-until" but instead consume bytes at a time, then simply open two devices and develop another plot. + +### Multi-Device, Blocking + +OK: current tech, here's one device with ye old' blocking code: + + + +Now let's get two up, using this blocking code, no hub: + + + + +With a hub, **note that some packets (in this sample of just 1000) land out of the normal distribution, up above 4500us per-packet-delay!** + + + + +and four devices on the hub, where **we catch a few more long-tail slow packets** + + + + + + +So, it seems basically that we are capped around 0.4MBit/sec in all of these scenarios, but introducing the hub sometimes casues packets to be delayed roughly 2x their normal delivery time. I figure that the hub is doing some switching / buffering that leads to this outcome... + +### Multi-Device, Asyncio + +"under the hood" asyncio should have basically the same performance as the blocking codes above, but I should test a prototype, if no other reason than to understand the design patterns. + + + + + + +So, once again no difference here **and** we still see some stragglers on the third and fourth plots. + +### Multi-Device, Multiprocessing + +To finish running through these tests, I want to try multi-processing which is true parallellism... + +There's a nice natural alignment with this and the rest of these systems (which are all serialized-codes to begin with), so it might be that this approach just suits: it allows us to carry on doing comms-stuff (like not missing acks and timeouts) while users write potentially blocking-codes (which seems to be semi-common in python). + +For future-architecture, my assumption is that I would do something like... one process does oversight, then we build one process per link layer, and probably even one per port, with user-application code going somewhere else. + +So, I presume this will be a lot heavier-handed programming wise, I'll put a stake down before carrying on. --- diff --git a/code/serial_multi_sink/cobs_usb_serial.py b/code/serial_multi_sink/cobs_usb_serial.py deleted file mode 100644 index 484d7454a445485cc3a26395fa728366d094a294..0000000000000000000000000000000000000000 --- a/code/serial_multi_sink/cobs_usb_serial.py +++ /dev/null @@ -1,17 +0,0 @@ -from cobs import cobs -import serial - - -class CobsUsbSerial: - def __init__(self, port, baudrate=115200): - self.port = port - self.ser = serial.Serial(port, baudrate=baudrate, timeout=1) - - def write(self, data: bytes): - data_enc = cobs.encode(data) + b"\x00" - self.ser.write(data_enc) - - def read(self): - data_enc = self.ser.read_until(b"\x00") - data = cobs.decode(data_enc[:-1]) - return data diff --git a/code/serial_multi_sink/multi_sink.py b/code/serial_multi_sink/multi_sink.py deleted file mode 100644 index 0c666ab025cf51367e01484fea956fe7e8b7ef80..0000000000000000000000000000000000000000 --- a/code/serial_multi_sink/multi_sink.py +++ /dev/null @@ -1,88 +0,0 @@ -from cobs_usb_serial import CobsUsbSerial -import asyncio -import struct -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -pck_len = 32 - -async def read_ser(cobs_serial, rx_func): - while True: - data = cobs_serial.read() - if len(data) == pck_len: - stamp = struct.unpack('=I', data[:4]) - rx_func(stamp) - await asyncio.sleep(0) - -def port_handler(stamp): - print(stamp) - -async def main(): - port_one = CobsUsbSerial("COM23") - - task_one = asyncio.create_task(read_ser(port_one, port_handler)) - - await asyncio.gather(task_one) - -asyncio.run(main()) - -# from cobs_usb_serial import CobsUsbSerial -# import struct -# import numpy as np -# import pandas as pd -# import matplotlib.pyplot as plt - -# ser = CobsUsbSerial("COM23") - -# stamp_count = 1000 -# pck_len = 250 - -# stamps = np.zeros(stamp_count) - -# for i in range(stamp_count): -# bts = ser.read() -# if len(bts) == pck_len: -# stamp = struct.unpack('=I', bts[:4]) -# stamps[i] = stamp[0] - -# print("stamps, ", stamps) - -# df = pd.DataFrame({'timestamps': stamps}) - -# df['deltas'] = df['timestamps'].diff() - -# # clean NaN's -# df = df.dropna() - -# # wipe obviously-wrong deltas (i.e. the 1st, which goes 0-start-us) -# df = df[df['deltas'] < 100000] - -# # Plotting -# fig, ax1 = plt.subplots(figsize=(11, 5)) - -# # Primary x-axis (time deltas) -# df['deltas'].plot(kind='hist', bins=100, ax=ax1) -# ax1.set_xlabel('Time-Stamp Deltas (us)') -# ax1.set_ylabel(f'Frequency (of {stamp_count})') - -# # Secondary x-axis (bandwidth) -# ax2 = ax1.twiny() -# ax2.set_xlabel('Estimated Bandwidth (Mbits/s)') - -# # Set the limits of the secondary axis based on the primary axis -# # new_tick_locations = np.linspace(df['deltas'].min(), df['deltas'].max(), num=len(ax1.get_xticks())) - -# # Convert tick locations to bandwidth -# # bandwidths = [(pck_len * 8) * (1e6 / x) for x in new_tick_locations] - -# x_ticks = ax1.get_xticks() -# bandwidth_ticks = [((pck_len * 8) * (1e6 / x)) / 1e6 for x in x_ticks] - -# ax2.set_xlim(max(bandwidth_ticks), min(bandwidth_ticks)) - -# plt.title(f'Single-Source COBS Data Sink Deltas, pck_len={pck_len}') - -# plt.tight_layout() - -# plt.show() diff --git a/code/serial_multi_sink_asyncio/cobs_usb_serial_async.py b/code/serial_multi_sink_asyncio/cobs_usb_serial_async.py new file mode 100644 index 0000000000000000000000000000000000000000..33ac5fce713030c46a629887c150d02739f220b2 --- /dev/null +++ b/code/serial_multi_sink_asyncio/cobs_usb_serial_async.py @@ -0,0 +1,35 @@ +import asyncio +from cobs import cobs +import serial + + +class CobsUsbSerial: + def __init__(self, port, baudrate=115200): + self.port = port + self.ser = serial.Serial(port, baudrate=baudrate, timeout=1) + self.buffer = bytearray() + + def write(self, data: bytes): + data_enc = cobs.encode(data) + b"\x00" + self.ser.write(data_enc) + + def read(self): + byte = self.ser.read(1) + if not byte: + return + if byte == b"\x00": + if len(self.buffer) > 0: + data = cobs.decode(self.buffer) + self.buffer = bytearray() + return data + else: + return + else: + self.buffer += byte + + async def attach(self, rx_func): + while True: + bts = self.read() + if bts: + rx_func(bts) + await asyncio.sleep(0) diff --git a/code/serial_multi_sink_asyncio/multi_sink_asyncio.py b/code/serial_multi_sink_asyncio/multi_sink_asyncio.py new file mode 100644 index 0000000000000000000000000000000000000000..7d9e9e3780a8f3c78f5e17cb8a2991dc2e9d7d8a --- /dev/null +++ b/code/serial_multi_sink_asyncio/multi_sink_asyncio.py @@ -0,0 +1,112 @@ +from cobs_usb_serial_async import CobsUsbSerial +import asyncio +import struct +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +stamp_count = 1000 +pck_len = 128 + +stamps_one = np.zeros(stamp_count) +counter_one = 0 +plot_one = False + +stamps_two = np.zeros(stamp_count) +counter_two = 0 +plot_two = False + +stamps_three = np.zeros(stamp_count) +counter_three = 0 +plot_three = False + +stamps_four = np.zeros(stamp_count) +counter_four = 0 +plot_four = False + +def plot_stamps(stamps): + # make df from stamps + df = pd.DataFrame({'timestamps': stamps}) + + # calculate deltas between stamps + df['deltas'] = df['timestamps'].diff() + + # clean NaN's + df = df.dropna() + + # wipe obviously-wrong deltas (i.e. the 1st, which goes 0-start-us) + df = df[df['deltas'] < 100000] + + # Plotting + fig, ax1 = plt.subplots(figsize=(11, 3)) + + ax1.set_xlim([1750, 4750]) + + # Primary x-axis (time deltas) + df['deltas'].plot(kind='hist', bins=100, ax=ax1) + ax1.set_xlabel('Time-Stamp Deltas (us) and equivalent (MBits/s)') + ax1.set_ylabel(f'Frequency (of {stamp_count})') + + # get axis ticks to calculate equivalent bandwidths + x_ticks = ax1.get_xticks() + ax1.set_xticks(x_ticks) + bandwidths = [((pck_len * 8) * (1e6 / x)) / 1e6 for x in x_ticks] + ticks = [] + + for i in range(len(x_ticks)): + print(i, x_ticks[i], bandwidths[i]) + ticks.append(f"{x_ticks[i]:.0f} ({bandwidths[i]:.3f})") + + ax1.set_xticklabels(ticks) + + plt.title(f'Single-Source COBS Data Sink Deltas, pck_len={pck_len}') + + plt.tight_layout() + + plt.show() + + +def cycle(data, stamps, counter, plot): + if counter >= stamp_count: + if not plot: + plot = True + plot_stamps(stamps) + return counter, plot + if len(data) == pck_len: + stamp = struct.unpack("=I", data[:4]) + stamps[counter] = stamp[0] + counter += 1 + return counter, plot + + +def port_one_handler(data): + global counter_one, plot_one + counter_one, plot_one = cycle(data, stamps_one, counter_one, plot_one) + +def port_two_handler(data): + global counter_two, plot_two + counter_two, plot_two = cycle(data, stamps_two, counter_two, plot_two) + +def port_three_handler(data): + global counter_three, plot_three + counter_three, plot_three = cycle(data, stamps_three, counter_three, plot_three) + +def port_four_handler(data): + global counter_four, plot_four + counter_four, plot_four = cycle(data, stamps_four, counter_four, plot_four) + + +async def main(): + port_one = CobsUsbSerial("COM23") + port_two = CobsUsbSerial("COM31") + port_three = CobsUsbSerial("COM33") + port_four = CobsUsbSerial("COM36") + + task_one = asyncio.create_task(port_one.attach(port_one_handler)) + task_two = asyncio.create_task(port_two.attach(port_two_handler)) + task_three = asyncio.create_task(port_three.attach(port_three_handler)) + task_four = asyncio.create_task(port_four.attach(port_four_handler)) + + await asyncio.gather(task_one, task_two, task_three, task_four) + +asyncio.run(main()) \ No newline at end of file diff --git a/code/serial_multi_sink_asyncio/multi_sink_blocking.py b/code/serial_multi_sink_asyncio/multi_sink_blocking.py new file mode 100644 index 0000000000000000000000000000000000000000..833f5be12216b5619ad3449abc0ae513d6fb1faf --- /dev/null +++ b/code/serial_multi_sink_asyncio/multi_sink_blocking.py @@ -0,0 +1,93 @@ +from cobs_usb_serial import CobsUsbSerial +import struct +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +ser_one = CobsUsbSerial("COM23") +ser_two = CobsUsbSerial("COM31") +ser_three = CobsUsbSerial("COM33") +ser_four = CobsUsbSerial("COM35") + +stamp_count = 1000 +pck_len = 128 + +stamps_one = np.zeros(stamp_count) +counter_one = 0 + +stamps_two = np.zeros(stamp_count) +counter_two = 0 + +stamps_three = np.zeros(stamp_count) +counter_three = 0 + +stamps_four = np.zeros(stamp_count) +counter_four = 0 + +def cycle(ser, stamps, counter): + if counter >= stamp_count: + return counter + bts = ser.read() + if bts: + if len(bts) == pck_len: + stamp = struct.unpack("=I", bts[:4]) + stamps[counter] = stamp[0] + counter += 1 + return counter + +while True: + counter_one = cycle(ser_one, stamps_one, counter_one) + counter_two = cycle(ser_two, stamps_two, counter_two) + counter_three = cycle(ser_three, stamps_three, counter_three) + counter_four = cycle(ser_four, stamps_four, counter_four) + + if counter_one == stamp_count and counter_two == stamp_count and counter_three == stamp_count and counter_four == stamp_count: + break + +# print("stamps, ", stamps_one, stamps_two) + +def plot_stamps(stamps): + # make df from stamps + df = pd.DataFrame({'timestamps': stamps}) + + # calculate deltas between stamps + df['deltas'] = df['timestamps'].diff() + + # clean NaN's + df = df.dropna() + + # wipe obviously-wrong deltas (i.e. the 1st, which goes 0-start-us) + df = df[df['deltas'] < 100000] + + # Plotting + fig, ax1 = plt.subplots(figsize=(11, 3)) + + ax1.set_xlim([1750, 4750]) + + # Primary x-axis (time deltas) + df['deltas'].plot(kind='hist', bins=100, ax=ax1) + ax1.set_xlabel('Time-Stamp Deltas (us) and equivalent (MBits/s)') + ax1.set_ylabel(f'Frequency (of {stamp_count})') + + # get axis ticks to calculate equivalent bandwidths + x_ticks = ax1.get_xticks() + ax1.set_xticks(x_ticks) + bandwidths = [((pck_len * 8) * (1e6 / x)) / 1e6 for x in x_ticks] + ticks = [] + + for i in range(len(x_ticks)): + print(i, x_ticks[i], bandwidths[i]) + ticks.append(f"{x_ticks[i]:.0f} ({bandwidths[i]:.3f})") + + ax1.set_xticklabels(ticks) + + plt.title(f'Single-Source COBS Data Sink Deltas, pck_len={pck_len}') + + plt.tight_layout() + + plt.show() + +plot_stamps(stamps_one) +plot_stamps(stamps_two) +plot_stamps(stamps_three) +plot_stamps(stamps_four) \ No newline at end of file diff --git a/code/serial_multi_sink/serial_list.py b/code/serial_multi_sink_asyncio/serial_list.py similarity index 100% rename from code/serial_multi_sink/serial_list.py rename to code/serial_multi_sink_asyncio/serial_list.py diff --git a/code/serial_multi_sink_blocking/cobs_usb_serial.py b/code/serial_multi_sink_blocking/cobs_usb_serial.py new file mode 100644 index 0000000000000000000000000000000000000000..818c2beb841dbf04fbc780625e76754b14a7e277 --- /dev/null +++ b/code/serial_multi_sink_blocking/cobs_usb_serial.py @@ -0,0 +1,30 @@ +from cobs import cobs +import serial + + +class CobsUsbSerial: + def __init__(self, port, baudrate=115200): + self.port = port + self.ser = serial.Serial(port, baudrate=baudrate, timeout=1) + self.buffer = bytearray() + + def write(self, data: bytes): + data_enc = cobs.encode(data) + b"\x00" + self.ser.write(data_enc) + + def read(self): + byte = self.ser.read(1) + if not byte: + return + if byte == b"\x00": + if len(self.buffer) > 0: + data = cobs.decode(self.buffer) + self.buffer = bytearray() + return data + else: + return + else: + self.buffer += byte + # data_enc = self.ser.read_until(b"\x00") + # data = cobs.decode(data_enc[:-1]) + # return data diff --git a/code/serial_multi_sink_blocking/multi_sink_blocking.py b/code/serial_multi_sink_blocking/multi_sink_blocking.py new file mode 100644 index 0000000000000000000000000000000000000000..833f5be12216b5619ad3449abc0ae513d6fb1faf --- /dev/null +++ b/code/serial_multi_sink_blocking/multi_sink_blocking.py @@ -0,0 +1,93 @@ +from cobs_usb_serial import CobsUsbSerial +import struct +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +ser_one = CobsUsbSerial("COM23") +ser_two = CobsUsbSerial("COM31") +ser_three = CobsUsbSerial("COM33") +ser_four = CobsUsbSerial("COM35") + +stamp_count = 1000 +pck_len = 128 + +stamps_one = np.zeros(stamp_count) +counter_one = 0 + +stamps_two = np.zeros(stamp_count) +counter_two = 0 + +stamps_three = np.zeros(stamp_count) +counter_three = 0 + +stamps_four = np.zeros(stamp_count) +counter_four = 0 + +def cycle(ser, stamps, counter): + if counter >= stamp_count: + return counter + bts = ser.read() + if bts: + if len(bts) == pck_len: + stamp = struct.unpack("=I", bts[:4]) + stamps[counter] = stamp[0] + counter += 1 + return counter + +while True: + counter_one = cycle(ser_one, stamps_one, counter_one) + counter_two = cycle(ser_two, stamps_two, counter_two) + counter_three = cycle(ser_three, stamps_three, counter_three) + counter_four = cycle(ser_four, stamps_four, counter_four) + + if counter_one == stamp_count and counter_two == stamp_count and counter_three == stamp_count and counter_four == stamp_count: + break + +# print("stamps, ", stamps_one, stamps_two) + +def plot_stamps(stamps): + # make df from stamps + df = pd.DataFrame({'timestamps': stamps}) + + # calculate deltas between stamps + df['deltas'] = df['timestamps'].diff() + + # clean NaN's + df = df.dropna() + + # wipe obviously-wrong deltas (i.e. the 1st, which goes 0-start-us) + df = df[df['deltas'] < 100000] + + # Plotting + fig, ax1 = plt.subplots(figsize=(11, 3)) + + ax1.set_xlim([1750, 4750]) + + # Primary x-axis (time deltas) + df['deltas'].plot(kind='hist', bins=100, ax=ax1) + ax1.set_xlabel('Time-Stamp Deltas (us) and equivalent (MBits/s)') + ax1.set_ylabel(f'Frequency (of {stamp_count})') + + # get axis ticks to calculate equivalent bandwidths + x_ticks = ax1.get_xticks() + ax1.set_xticks(x_ticks) + bandwidths = [((pck_len * 8) * (1e6 / x)) / 1e6 for x in x_ticks] + ticks = [] + + for i in range(len(x_ticks)): + print(i, x_ticks[i], bandwidths[i]) + ticks.append(f"{x_ticks[i]:.0f} ({bandwidths[i]:.3f})") + + ax1.set_xticklabels(ticks) + + plt.title(f'Single-Source COBS Data Sink Deltas, pck_len={pck_len}') + + plt.tight_layout() + + plt.show() + +plot_stamps(stamps_one) +plot_stamps(stamps_two) +plot_stamps(stamps_three) +plot_stamps(stamps_four) \ No newline at end of file diff --git a/code/serial_multi_sink_blocking/serial_list.py b/code/serial_multi_sink_blocking/serial_list.py new file mode 100644 index 0000000000000000000000000000000000000000..5f25377227f77c79dc2db925a16f5a5a280f412b --- /dev/null +++ b/code/serial_multi_sink_blocking/serial_list.py @@ -0,0 +1,20 @@ +import serial.tools.list_ports + +def list_serial_ports(): + ports = serial.tools.list_ports.comports() + for port in ports: + print(f"Port: {port.device}") + print(f" - Description: {port.description}") + if port.serial_number: + print(f" - Serial Number: {port.serial_number}") + if port.manufacturer: + print(f" - Manufacturer: {port.manufacturer}") + if port.product: + print(f" - Product: {port.product}") + if port.vid is not None: + print(f" - VID: {port.vid:04X}") + if port.pid is not None: + print(f" - PID: {port.pid:04X}") + print() + +list_serial_ports() diff --git a/code/serial_source/serial_source.ino b/code/serial_source/serial_source.ino index 610940615df3a2decc89f341a90fe774b12ec599..0096632b34d13134fbd650b6c101bd97404c4356 100644 --- a/code/serial_source/serial_source.ino +++ b/code/serial_source/serial_source.ino @@ -40,7 +40,7 @@ void loop() { // tx a stamp AFAP if(cobs.clearToSend()){ chunk.u = micros(); - cobs.send(chunk.bytes, 250); + cobs.send(chunk.bytes, 128); digitalWrite(PIN_LED_G, !digitalRead(PIN_LED_G)); } // blink to see hangups diff --git a/images/2023-12-27_asyncio-4-devices-01.png b/images/2023-12-27_asyncio-4-devices-01.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff528ae3a6e9b1197ef8ec73af7f0ab8a52ff96 Binary files /dev/null and b/images/2023-12-27_asyncio-4-devices-01.png differ diff --git a/images/2023-12-27_asyncio-4-devices-02.png b/images/2023-12-27_asyncio-4-devices-02.png new file mode 100644 index 0000000000000000000000000000000000000000..09d4af94c95d972344748d6de2725973f22d753e Binary files /dev/null and b/images/2023-12-27_asyncio-4-devices-02.png differ diff --git a/images/2023-12-27_asyncio-4-devices-03.png b/images/2023-12-27_asyncio-4-devices-03.png new file mode 100644 index 0000000000000000000000000000000000000000..665df19c5aa7cb64c6b878913872cba2d449116f Binary files /dev/null and b/images/2023-12-27_asyncio-4-devices-03.png differ diff --git a/images/2023-12-27_asyncio-4-devices-04.png b/images/2023-12-27_asyncio-4-devices-04.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9bfbe5b9e9db6f7b66d7a3b4972b98ca23f769 Binary files /dev/null and b/images/2023-12-27_asyncio-4-devices-04.png differ diff --git a/images/2023-12-27_blocking-1-device.png b/images/2023-12-27_blocking-1-device.png new file mode 100644 index 0000000000000000000000000000000000000000..6cebe3ddd409128bbc8b3647d34caa4bf57bc40c Binary files /dev/null and b/images/2023-12-27_blocking-1-device.png differ diff --git a/images/2023-12-27_blocking-2-devices-01.png b/images/2023-12-27_blocking-2-devices-01.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4d4590c27f8591c2968fe1b72495517ea664e6 Binary files /dev/null and b/images/2023-12-27_blocking-2-devices-01.png differ diff --git a/images/2023-12-27_blocking-2-devices-02.png b/images/2023-12-27_blocking-2-devices-02.png new file mode 100644 index 0000000000000000000000000000000000000000..e94d3a3550b3eb60c8005edc2a2bcf343ab148aa Binary files /dev/null and b/images/2023-12-27_blocking-2-devices-02.png differ diff --git a/images/2023-12-27_blocking-2-hub-devices-01.png b/images/2023-12-27_blocking-2-hub-devices-01.png new file mode 100644 index 0000000000000000000000000000000000000000..e29d74af5bcf58e32046c05995657a5163e98e67 Binary files /dev/null and b/images/2023-12-27_blocking-2-hub-devices-01.png differ diff --git a/images/2023-12-27_blocking-2-hub-devices-02.png b/images/2023-12-27_blocking-2-hub-devices-02.png new file mode 100644 index 0000000000000000000000000000000000000000..43ffeb2009858b0473996159a22e7d4b028a6cbc Binary files /dev/null and b/images/2023-12-27_blocking-2-hub-devices-02.png differ diff --git a/images/2023-12-27_blocking-4-devices-01.png b/images/2023-12-27_blocking-4-devices-01.png new file mode 100644 index 0000000000000000000000000000000000000000..2c76c8eb96e9dad6f2a7226285dcf9226fe23eea Binary files /dev/null and b/images/2023-12-27_blocking-4-devices-01.png differ diff --git a/images/2023-12-27_blocking-4-devices-02.png b/images/2023-12-27_blocking-4-devices-02.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b241851617cbfdc35332df84985c5b44cd2878 Binary files /dev/null and b/images/2023-12-27_blocking-4-devices-02.png differ diff --git a/images/2023-12-27_blocking-4-devices-03.png b/images/2023-12-27_blocking-4-devices-03.png new file mode 100644 index 0000000000000000000000000000000000000000..83f66a652ad551379c3433d98cd7d1627945c078 Binary files /dev/null and b/images/2023-12-27_blocking-4-devices-03.png differ diff --git a/images/2023-12-27_blocking-4-devices-04.png b/images/2023-12-27_blocking-4-devices-04.png new file mode 100644 index 0000000000000000000000000000000000000000..604e7072b1360c1e4d19d53d36b25678c3b348ff Binary files /dev/null and b/images/2023-12-27_blocking-4-devices-04.png differ