<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="https://community.element14.com/cfs-file/__key/system/syndication/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Forum - Recent Threads</title><link>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum</link><description /><dc:language>en-US</dc:language><generator>Telligent Community 12</generator><lastBuildDate>Tue, 27 Oct 2026 14:38:51 GMT</lastBuildDate><atom:link rel="self" type="application/rss+xml" href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum" /><item><title>RE: VitaRF - Part 9 - Sleep and Fall Detection</title><link>https://community.element14.com/thread/238798?ContentTypeID=1</link><pubDate>Tue, 27 Oct 2026 14:38:51 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:a0f9f9f2-cb30-479e-820e-8a9c1fd1d2a6</guid><dc:creator>arvindsa</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238798?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57388/vitarf---part-9---sleep-and-fall-detection/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;It can be fixed,&amp;nbsp; but considering the deadline, not worth it.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>VitaRF - Part 9 - Sleep and Fall Detection</title><link>https://community.element14.com/thread/57388?ContentTypeID=0</link><pubDate>Mon, 26 Oct 2026 04:29:02 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:72f9e1c6-f0ec-4f01-a05d-e95e2ed83304</guid><dc:creator>arvindsa</dc:creator><slash:comments>4</slash:comments><comments>https://community.element14.com/thread/57388?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57388/vitarf---part-9---sleep-and-fall-detection/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;&lt;strong&gt;Recap:&lt;/strong&gt; VitaRF is a wireless vitals node (heart rate, SpO₂, temperature) that reports over BLE into Home Assistant, so someone can keep an eye on a loved one. It&amp;#39;s built in three phases: a development-kit bring-up, a custom nRF52832 PCB, and a Matter over Thread proof of concept.&lt;/p&gt;
&lt;p&gt;Past Posts&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57254/vitarf---part-1---the-plan" data-e14adj="t"&gt;VitaRF - Part 1 - The Plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57279/vitarf---part-2---interface-max30102-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 2 - Interface MAX30102 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57291/vitarf---part-3---interfacing-max30208-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 3 - Interfacing MAX30208 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57293/vitarf---part-4---sending-sensor-data-to-home-assistant-via-ble-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 4 - Sending Sensor Data to Home Assistant via BLE on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57317/vitarf---part-5---matter-over-thread-on-nrf54l15-with-home-assistant" data-e14adj="t"&gt;VitaRF - Part 5 - Matter over Thread on nRF54L15 with Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57331/vitarf---part-6---nrf52-based-heart-rate-temperature-wearable-pcb-for-home-assistant" data-e14adj="t"&gt;VitaRF - Part 6 - An nRF52-Based Heart-Rate &amp;amp; Temperature Wearable PCB for Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57337/vitarf---part-7---adding-a-philips-wiz-smart-bulb-to-home-assistant" data-e14adj="t"&gt;VitaRF - Part 7 - Adding a Philips WiZ Smart Bulb to Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/p/addpost/TODO-LINK-PART-8" data-e14adj="t"&gt;VitaRF - Part 8 - Assembling the PCB with Stencil, Paste and a Hot Plate&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Heart rate, SpO₂ and temperature tell you about someone&amp;#39;s body. The accelerometer tells you what they are doing. For someone living alone, two questions matter: did they fall, and are they sleeping normally? Both can be answered from the same 50 Hz stream of wrist acceleration.&lt;/p&gt;
&lt;p&gt;This post covers the whole chain: the LIS2DE12 driver, the sleep scoring, the fall detector, how they run together on the node, and what has and hasn&amp;#39;t been validated.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x720/__key/communityserver-discussions-components-files/458/fig_2D00_01_2D00_data_2D00_path.png" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r6k6pu0"&gt;Part A &amp;mdash; The LIS2DE12&lt;/h2&gt;
&lt;h3 id="mcetoc_1k5r6k6pu1"&gt;Wiring&lt;/h3&gt;
&lt;p&gt;The LIS2DE12 is a 2 &amp;times; 2 mm LGA-12 three-axis accelerometer that draws about 6 &amp;micro;A at 50 Hz. It is an 8-bit part. That&amp;#39;s enough for both jobs, as the sleep section shows.&lt;/p&gt;
&lt;p&gt;It sits on the shared I2C bus, which already carries the MAX30208, MAX30102, nPM1300 and DRV2605L. So it needs no extra pins, and 50 Hz &amp;times; 3 bytes is a small load for 400 kHz I2C.&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Connection&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VDD / VDD_IO&lt;/td&gt;
&lt;td&gt;+3V3 (nPM1300 BUCK2, always on)&lt;/td&gt;
&lt;td&gt;Same rail as the I2C pull-ups, so it can&amp;#39;t be back-powered through the bus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDA / SCL&lt;/td&gt;
&lt;td&gt;Shared I2C bus&lt;/td&gt;
&lt;td&gt;Address &lt;strong&gt;0x19&lt;/strong&gt; (SA0 tied high)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CS&lt;/td&gt;
&lt;td&gt;Tied &lt;strong&gt;high&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Selects I2C mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT1&lt;/td&gt;
&lt;td&gt;P0.12&lt;/td&gt;
&lt;td&gt;Free-fall engine, push-pull, active high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT2&lt;/td&gt;
&lt;td&gt;P0.13&lt;/td&gt;
&lt;td&gt;Motion engine, push-pull, active high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The chip has two independent interrupt engines, and the two jobs need different logic. Free-fall is an &lt;strong&gt;AND&lt;/strong&gt; of all three axes being near 0 g. Motion, such as rolling over in bed, is an &lt;strong&gt;OR&lt;/strong&gt; of any axis passing a threshold. Each engine gets its own pin.&lt;/p&gt;
&lt;p&gt;TODO-PHOTO: close-up of U2 (LIS2DE12) on the assembled board&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6k6pu2"&gt;Driver init&lt;/h3&gt;
&lt;p&gt;The driver (&lt;code&gt;firmware/lib/lis2de12.c&lt;/code&gt;, nRF5 SDK 17.1.0) checks &lt;code&gt;WHO_AM_I&lt;/code&gt; and then writes every control register explicitly, rather than trusting reset values. A warm re-init always lands in the same state:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:59a41995-0c70-46c7-adb8-46913ad9fc19:type=c_cpp&amp;text=bool%20lis2de12_init%28lis2de12_odr_t%20odr%2C%20lis2de12_fs_t%20fs%29%0A%7B%0A%20%20%20%20uint8_t%20id%3B%0A%20%20%20%20if%20%28rd%28REG_WHO_AM_I%2C%20%26id%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20id%20%21%3D%20WHO_AM_I_VALUE%29%20%20%20%2F%2A%200x33%20%2A%2F%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%20%20%20%20...%0A%20%20%20%20if%20%28wr%28REG_CTRL1%2C%20CTRL1_LPEN%20%7C%20CTRL1_XYZ_EN%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%2F%2A%20ODR%20%3D%20power-down%20%2A%2F%0A%20%20%20%20%20%20%20%20wr%28REG_CTRL2%2C%200x00%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2A%20high-pass%20off%20everywhere%20%2A%2F%0A%20%20%20%20%20%20%20%20wr%28REG_CTRL3%2C%200x00%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2A%20INT1%20pin%3A%20nothing%20routed%20%2A%2F%0A%20%20%20%20%20%20%20%20wr%28REG_CTRL4%2C%20%28uint8_t%29%28CTRL4_BDU%20%7C%20%28%28uint8_t%29fs%20%3C%3C%204%29%29%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%0A%20%20%20%20%20%20%20%20wr%28REG_CTRL5%2C%20CTRL5_LIR_INT1%20%7C%20CTRL5_LIR_INT2%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%2F%2A%20latched%20%2A%2F%0A%20%20%20%20%20%20%20%20wr%28REG_CTRL6%2C%200x00%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2A%20INT2%20pin%3A%20nothing%20routed%2C%20active-high%20%2A%2F%0A%20%20%20%20%20%20%20%20wr%28REG_FIFO_CTRL%2C%200x00%29%20%21%3D%20NRF_SUCCESS%20%7C%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2A%20FIFO%20bypass%20%2A%2F%0A%20%20%20%20%20%20%20%20...%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20lis2de12_set_odr%28odr%29%20%3D%3D%20NRF_SUCCESS%3B%0A%7D]&lt;/p&gt;
&lt;p&gt;A few datasheet points the driver depends on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CTRL_REG1.LPen&lt;/code&gt; must be &lt;strong&gt;1&lt;/strong&gt; for correct operation, even though this part has no low-power/high-resolution split.&lt;/li&gt;
&lt;li&gt;The output steps (15.6 / 31.2 / 62.5 mg per LSB) and the interrupt threshold steps (16 / 32 / 62 / 186 mg) are different numbers, so the driver converts thresholds with its own table.&lt;/li&gt;
&lt;li&gt;Boot completes within 5 ms of power-up, so the BOOT bit isn&amp;#39;t needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5r6lecb3"&gt;Reading samples&lt;/h3&gt;
&lt;p&gt;The data is 8-bit, but the register map keeps the 16-bit layout of the bigger parts. A six-byte burst from 0x28 returns X, Y and Z, with the value in the odd bytes (&lt;code&gt;OUT_X_H&lt;/code&gt;, &lt;code&gt;OUT_Y_H&lt;/code&gt;, &lt;code&gt;OUT_Z_H&lt;/code&gt;):&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:42847816-3bc7-40c1-85a7-d2881c6e2c3a:type=c_cpp&amp;text=ret_code_t%20lis2de12_read_raw%28lis2de12_raw_t%20%2Aout%29%0A%7B%0A%20%20%20%20uint8_t%20buf%5B6%5D%3B%0A%20%20%20%20TRY%28i2c_bus_read_reg%28s_dev%2C%20REG_FIFO_READ%20%7C%20AUTO_INC%2C%20buf%2C%20sizeof%28buf%29%29%29%3B%0A%20%20%20%20out-%3Ex%20%3D%20%28int8_t%29buf%5B1%5D%3B%0A%20%20%20%20out-%3Ey%20%3D%20%28int8_t%29buf%5B3%5D%3B%0A%20%20%20%20out-%3Ez%20%3D%20%28int8_t%29buf%5B5%5D%3B%0A%20%20%20%20return%20NRF_SUCCESS%3B%0A%7D]&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6ma244"&gt;The FIFO&lt;/h3&gt;
&lt;p&gt;Waking the CPU 50 times a second to read 3 bytes would waste power. The LIS2DE12 has a 32-sample FIFO. In stream mode it keeps the newest 32 samples, which at 50 Hz is 640 ms of history. The node reads it every &lt;strong&gt;400 ms&lt;/strong&gt; in one I2C burst of up to 192 bytes.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FIFO_SRC&lt;/code&gt; count field is one less than the number of unread samples. So 31 means 32 are stored, and EMPTY clear with a count of 0 means one sample is waiting. The driver handles it like this:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:4271493f-3872-4bd6-bbab-21a61830b4e4:type=c_cpp&amp;text=%2F%2A%20FSS%20is%20one%20less%20than%20the%20number%20of%20unread%20sample%20sets%20%2831%20when%20full%2C%0A%20%2A%2032%20stored%3B%200%20with%20EMPTY%20clear%20when%20exactly%20one%20is%20waiting%29.%20EMPTY%20says%0A%20%2A%20there%20is%20nothing%20at%20all.%20%2A%2F%0Auint8_t%20n%20%3D%20%28src%20%26%20FIFO_SRC_EMPTY%29%20%3F%200%20%3A%20%28uint8_t%29%28%28src%20%26%20FIFO_SRC_FSS_MASK%29%20%2B%201%29%3B%0A...%0Auint8_t%20raw%5BFIFO_DEPTH%20%2A%206%5D%3B%0ATRY%28i2c_bus_read_reg%28s_dev%2C%20REG_FIFO_READ%20%7C%20AUTO_INC%2C%20raw%2C%20%28uint8_t%29%28n%20%2A%206%29%29%29%3B%0Afor%20%28uint8_t%20i%20%3D%200%3B%20i%20%3C%20n%3B%20i%2B%2B%29%0A%7B%0A%20%20%20%20buf%5Bi%5D.x%20%3D%20%28int8_t%29raw%5Bi%20%2A%206%20%2B%201%5D%3B%0A%20%20%20%20buf%5Bi%5D.y%20%3D%20%28int8_t%29raw%5Bi%20%2A%206%20%2B%203%5D%3B%0A%20%20%20%20buf%5Bi%5D.z%20%3D%20%28int8_t%29raw%5Bi%20%2A%206%20%2B%205%5D%3B%0A%7D]&lt;/p&gt;
&lt;p&gt;On the board, the FIFO gives 15 samples after 300 ms, a full 32 with the overrun flag after one second, and no loss when read every 400 ms.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6n3sm5"&gt;Interrupts&lt;/h3&gt;
&lt;p&gt;Both interrupt engines run in latched mode. The pin stays high until the matching &lt;code&gt;INTx_SRC&lt;/code&gt; register is read, so an event can&amp;#39;t be lost while the CPU is busy. The GPIOTE handler only sets a flag. All I2C traffic happens later, in normal context:&amp;#39;&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:20c9a0ec-e9b9-437b-8cf1-52df4a86b11a:type=c_cpp&amp;text=static%20void%20gpiote_handler%28nrfx_gpiote_pin_t%20pin%2C%20nrf_gpiote_polarity_t%20action%29%0A%7B%0A%20%20%20%20if%20%28pin%20%3D%3D%20s_pin1%29%20__atomic_fetch_or%28%26s_flags%2C%20FLAG_INT1%2C%20__ATOMIC_ACQ_REL%29%3B%0A%20%20%20%20if%20%28pin%20%3D%3D%20s_pin2%29%20__atomic_fetch_or%28%26s_flags%2C%20FLAG_INT2%2C%20__ATOMIC_ACQ_REL%29%3B%0A%7D]&lt;/p&gt;
&lt;p&gt;A latched pin that is already high at start-up gives no further edge, so &lt;code&gt;lis2de12_irq_init()&lt;/code&gt; also reads the pin level once when the driver starts.&lt;/p&gt;
&lt;p&gt;Two details matter for the thresholds. The high-pass motion flag (ZH) compares the absolute value, so it catches movement in both directions. And &lt;code&gt;INTx_SRC&lt;/code&gt; reports flags for every axis, whether or not that axis is enabled, so the driver masks them with &lt;code&gt;INTx_CFG&lt;/code&gt; before acting.&lt;/p&gt;
&lt;p&gt;The node&amp;#39;s boot test uses the chip&amp;#39;s built-in self-test, which applies a test force and checks the output shift against the datasheet window. The hardware free-fall interrupt (350 mg, 30 ms) is also configured.&lt;/p&gt;
&lt;p&gt;For now the firmware drains the FIFO on a 400 ms timer and runs both algorithms in software. The interrupt pins are wired and supported by the driver. Using INT1 and INT2 to wake the MCU from deeper sleep is the next power optimisation.&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r6nvv26"&gt;Part B &amp;mdash; Sleep quality&lt;/h2&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:800px;max-width:400px;" alt=" " src="https://community.element14.com/resized-image/__size/800x1600/__key/communityserver-discussions-components-files/458/fig_2D00_02_2D00_sleep_2D00_pipeline.png" /&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6p8kr7"&gt;The approach&lt;/h3&gt;
&lt;p&gt;Sleep tracking from a wrist sensor is called actigraphy, and sleep researchers have used it since the 1990s. Commercial watches add heart rate and proprietary models to stage light, deep and REM sleep. I used the published and Open route:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;raw acceleration &amp;rarr; activity counts &amp;rarr; sleep/wake per minute &amp;rarr; night summary&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every step has a published method and a reference implementation to test against. It tells sleep from wake, not sleep stages. For a caregiver, &amp;quot;slept about seven hours, woke up three times&amp;quot; is the useful part.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6p8kr8"&gt;Step 1 &amp;mdash; Activity counts&lt;/h3&gt;
&lt;p&gt;Sleep-scoring algorithms were designed on ActiGraph &amp;quot;activity counts&amp;quot;, a per-minute number that ActiGraph devices have produced for decades. Their thresholds only mean something on those numbers. In 2022 ActiGraph published the counting algorithm (Neishabouri et al., &lt;em&gt;Scientific Reports&lt;/em&gt; 12:11958). Per axis, the pipeline is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Resample to 30 Hz. The LIS2DE12 runs at 50 Hz, so the signal is upsampled by 3, low-pass filtered, and every fifth sample kept.&lt;/li&gt;
&lt;li&gt;A fixed 0.29&amp;ndash;1.63 Hz IIR band-pass. This keeps human movement and removes gravity and vibration.&lt;/li&gt;
&lt;li&gt;Scale, then a dead-band (anything below 4 becomes 0) and a clip at 128.&lt;/li&gt;
&lt;li&gt;Average groups of three (30 Hz &amp;rarr; 10 Hz) and sum over the minute.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The code is a streaming C library (&lt;code&gt;firmware/lib/actcounts.c&lt;/code&gt;). It keeps only the filter state, so memory use stays fixed however long it runs. The core is the band-pass:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:79f3bd79-6ce5-4742-9fc6-83b7f79782d8:type=c_cpp&amp;text=%2F%2A%20Band-pass%20filter%2C%20published%20constants%20%28Neishabouri%20et%20al.%202022%29.%20The%0A%20%2A%20filter%20is%20specified%20at%2030%20Hz%3B%20everything%20is%20resampled%20to%2030%20Hz%20first.%20%2A%2F%0Astatic%20const%20actcounts_real_t%20k_b%5B8%5D%20%3D%20%7B%0A%20%20%20%20-0.009341062898525%2C%20-0.025470289659360%2C%20-0.004235264826105%2C%20%200.044152415456420%2C%0A%20%20%20%20%200.036493718347760%2C%20-0.011893961934740%2C%20-0.022917390623150%2C%20-0.006788163862310%2C%0A%7D%3B%0A%2F%2A%20Feedback%20taps%20a1..a7%20%28a0%20%3D%201%29.%20%2A%2F%0Astatic%20const%20actcounts_real_t%20k_a%5B7%5D%20%3D%20%7B%0A%20%20%20%20-3.63367395910957%2C%205.03689812757486%2C%20-3.09612247819666%2C%200.50620507633883%2C%0A%20%20%20%20%200.32421701566682%2C%20-0.15685485875559%2C%200.01949130205890%2C%0A%7D%3B]&lt;/p&gt;
&lt;p&gt;followed by the dead-band and clip:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:670bafd1-4758-4766-a6c5-4ceb49d2ea4b:type=c_cpp&amp;text=actcounts_real_t%20v%20%3D%20fabs%28bpf_step%28ac%2C%20x%29%20%2A%20GAIN%29%3B%0Aif%20%28v%20%3E%20CLIP%29%20%20%20%20%20%20%20%20%20%20v%20%3D%20CLIP%3B%20%20%20%20%20%20%2F%2A%20128%20%2A%2F%0Aelse%20if%20%28v%20%3C%20DEADBAND%29%20v%20%3D%200.0%3B%20%20%20%20%20%20%20%2F%2A%204%20%2A%2F%0Aelse%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20v%20%3D%20floor%28v%29%3B]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Checking against ActiGraph.&lt;/strong&gt; ActiGraph&amp;#39;s reference repository includes recordings with the counts its ActiLife software produced for them. I ran the C code on those recordings and compared it epoch by epoch:&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30 Hz and 40 Hz recordings, 3 axes, 3000 epochs each&lt;/td&gt;
&lt;td&gt;Exact, except one epoch per recording, off by 1 count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 Hz path (the node&amp;#39;s rate; no reference data at 50 Hz) vs. a transcription of the reference&lt;/td&gt;
&lt;td&gt;299 of 300 epochs exact, one off by 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The off-by-one cases come from floating-point summation order on a rounding boundary. The filter runs in &lt;code&gt;double&lt;/code&gt;. In single precision about 40 % of epochs differed from ActiLife, by up to 5 counts, because IIR filters with poles this close to the unit circle are sensitive to precision. The nRF52832 FPU is single-precision, so &lt;code&gt;double&lt;/code&gt; runs in software. At 50 samples per second per axis, the cost is estimated at about 0.2 % of CPU. That estimate isn&amp;#39;t measured yet.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6qond9"&gt;Why 8 bits is enough&lt;/h3&gt;
&lt;p&gt;ActiGraph devices use a 12-bit accelerometer. The LIS2DE12 has 31 mg per step at &amp;plusmn;4 g. To check whether that coarseness matters, I fed a 1 Hz sine of different amplitudes through the counts algorithm at each resolution (counts per minute):&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Movement amplitude&lt;/th&gt;
&lt;th&gt;Ideal sensor&lt;/th&gt;
&lt;th&gt;&amp;plusmn;2 g (15.6 mg/LSB)&lt;/th&gt;
&lt;th&gt;&amp;plusmn;4 g (31.2 mg/LSB)&lt;/th&gt;
&lt;th&gt;&amp;plusmn;8 g (62.5 mg/LSB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 mg&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75 mg&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 mg&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;1140&lt;/td&gt;
&lt;td&gt;1440&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 mg&lt;/td&gt;
&lt;td&gt;3720&lt;/td&gt;
&lt;td&gt;3720&lt;/td&gt;
&lt;td&gt;3600&lt;/td&gt;
&lt;td&gt;3600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 mg&lt;/td&gt;
&lt;td&gt;8160&lt;/td&gt;
&lt;td&gt;8160&lt;/td&gt;
&lt;td&gt;8160&lt;/td&gt;
&lt;td&gt;7920&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The counts algorithm already reports zero for movements below about 60&amp;ndash;75 mg, even with a perfect sensor. The LIS2DE12&amp;#39;s step size sits at that same floor, so the resolution costs little for sleep. One range, &amp;plusmn;4 g, serves both sleep and falls. Falls need headroom above 2 g for the impact, and there&amp;#39;s no need to switch ranges.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6qonda"&gt;Step 2 &amp;mdash; Sleep or wake, minute by minute&lt;/h3&gt;
&lt;p&gt;Scoring uses &lt;strong&gt;Cole&amp;ndash;Kripke&lt;/strong&gt; (Cole et al., &lt;em&gt;Sleep&lt;/em&gt; 15(5), 1992). It looks at a 7-minute window around each minute and takes a weighted sum of the activity:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:227e0228-0034-4b50-bf3d-0c65c47dd955:type=text&amp;text=SI%20%3D%200.001%20%C3%97%20%28106%C2%B7A%E2%82%8B%E2%82%84%20%2B%2054%C2%B7A%E2%82%8B%E2%82%83%20%2B%2058%C2%B7A%E2%82%8B%E2%82%82%20%2B%2076%C2%B7A%E2%82%8B%E2%82%81%20%2B%20230%C2%B7A%E2%82%80%20%2B%2074%C2%B7A%E2%82%8A%E2%82%81%20%2B%2067%C2%B7A%E2%82%8A%E2%82%82%29]&lt;/p&gt;
&lt;p&gt;Here A is the minute&amp;#39;s count &amp;divide; 100, capped at 300. A minute is **asleep if SI &amp;lt; 1**. The current minute carries the most weight, and the minutes before it count more than the ones after.&lt;/p&gt;
&lt;p&gt;I chose Cole&amp;ndash;Kripke over Sadeh for three reasons. It was developed on wrist actigraphy in adults, which is this use case. It&amp;#39;s a fixed seven-tap filter that is cheap and easy to check by hand. And there&amp;#39;s a clear open-source reference (the `actigraph.sleepr` R package) to compare against.&lt;/p&gt;
&lt;p&gt;On the MCU the comparison needs no floating point. Multiply both sides by 100 000, and it becomes integer arithmetic:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:8426405b-ba88-45f4-a8f5-5065393839d6:type=c_cpp&amp;text=%2F%2A%20Counts%20are%20divided%20by%20100%20and%20capped%20at%20300%2C%20i.e.%20capped%20at%2030000%20before%20the%0A%20%2A%20%28exact%29%20division.%20SI%20%3C%201%20%20%3C%3D%3E%20%20sum%28w%20%2A%20min%28c%2C%2030000%29%29%20%3C%20100000.%20%2A%2F%0Astatic%20const%20uint16_t%20k_ck_weight%5B7%5D%20%3D%20%7B%20106%2C%2054%2C%2058%2C%2076%2C%20230%2C%2074%2C%2067%20%7D%3B%0A%0Abool%20sleepq_cole_kripke_asleep%28const%20uint16_t%20%2Acounts%2C%20uint16_t%20n%2C%20uint16_t%20i%29%0A%7B%0A%20%20%20%20uint32_t%20sum%20%3D%200%3B%0A%20%20%20%20for%20%28int8_t%20k%20%3D%200%3B%20k%20%3C%207%3B%20k%2B%2B%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20int32_t%20idx%20%3D%20%28int32_t%29i%20-%204%20%2B%20k%3B%0A%20%20%20%20%20%20%20%20if%20%28idx%20%3C%200%20%7C%7C%20idx%20%3E%3D%20%28int32_t%29n%29%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%20%2F%2A%20zero%20padding%20%2A%2F%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20uint32_t%20c%20%3D%20counts%5Bidx%5D%3B%0A%20%20%20%20%20%20%20%20if%20%28c%20%3E%2030000UL%29%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20c%20%3D%2030000UL%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20sum%20%2B%3D%20%28uint32_t%29k_ck_weight%5Bk%5D%20%2A%20c%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20sum%20%3C%20100000UL%3B%0A%7D]&lt;/p&gt;
&lt;p&gt;Against the published formula in floating point, on 15 000 random minutes, the integer version gives identical results.&lt;/p&gt;
&lt;p&gt;Each minute needs the two minutes after it, so the live sleep/wake decision trails real time by two minutes. For sleep, that doesn&amp;#39;t matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which axis?&lt;/strong&gt; ActiGraph scores the axis along the forearm. How the node sits on the wrist depends on the strap and the person, so the node uses the &lt;strong&gt;vector magnitude&lt;/strong&gt; of the three per-axis counts. It doesn&amp;#39;t depend on orientation, but it reads slightly higher than one axis, so it leans a little towards scoring wake.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6sf27b"&gt;Step 3 &amp;mdash; The night summary&lt;/h3&gt;
&lt;p&gt;Per-minute sleep/wake is noisy on its own, so the night summary turns a bed-time window into the standard actigraphy numbers:&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time in bed&lt;/td&gt;
&lt;td&gt;Bed mark &amp;rarr; up mark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep onset latency&lt;/td&gt;
&lt;td&gt;Bed &amp;rarr; the first run of 10 consecutive sleep minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total sleep time&lt;/td&gt;
&lt;td&gt;Sleep minutes between onset and the final awakening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WASO&lt;/td&gt;
&lt;td&gt;&amp;quot;Wake after sleep onset&amp;quot;: awake minutes between onset and final awakening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awakenings&lt;/td&gt;
&lt;td&gt;Wake runs of 5 minutes or longer after onset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep efficiency&lt;/td&gt;
&lt;td&gt;Total sleep &amp;divide; time in bed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The 10- and 5-minute run lengths are compile-time constants in &lt;code&gt;sleepq.h&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How does the node know when you went to bed?&lt;/strong&gt; It doesn&amp;#39;t, and the algorithm can&amp;#39;t reliably tell either. Lying on the sofa watching TV looks like sleep to an accelerometer. So the wearer marks it. A &lt;strong&gt;double press&lt;/strong&gt; on the button marks bed time, and the LED flashes cyan. The next double press marks getting up. That press scores the remaining minutes, builds the summary, writes it to the flash log, and sends the sleep minutes to Home Assistant.&lt;/p&gt;
&lt;p&gt;The pipeline runs all the time. Home Assistant also receives a live activity count and an asleep/awake flag every minute. The node keeps 12 hours of minutes: 720 &amp;times; 2 bytes of counts, plus one bit per minute for the decision, about 1.5 KB. If the buffer fills during the day it starts over. At bed time it starts fresh if less than 10 hours of room is left, so a night never runs out of space.&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r6sf27c"&gt;Part C &amp;mdash; Fall detection&lt;/h2&gt;
&lt;h3 id="mcetoc_1k5r6sf27d"&gt;The approach&lt;/h3&gt;
&lt;p&gt;A fall seen from an accelerometer has a typical shape, and most fall-detection work (Bourke et al. 2007, Kangas et al. 2008, the SisFall dataset by Sucerquia et al. 2017) uses a version of it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Free-fall:&lt;/strong&gt; while the body drops, the sensor reads less than 1 g.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Impact:&lt;/strong&gt; a sharp spike when the body hits the floor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aftermath:&lt;/strong&gt; the person lies still.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything is measured on the &lt;strong&gt;signal vector magnitude&lt;/strong&gt;, SVM = &amp;radic;(x&amp;sup2; + y&amp;sup2; + z&amp;sup2;), which reads about 1000 mg at rest in any orientation. I used a threshold detector rather than a trained classifier. It fits in a few hundred bytes, every decision can be explained, and its failure modes are known. A classifier would need a labelled set of wrist falls, which doesn&amp;#39;t exist yet.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x720/__key/communityserver-discussions-components-files/458/fig_2D00_03_2D00_fall_2D00_state_2D00_machine.png" /&gt;&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free-fall threshold&lt;/td&gt;
&lt;td&gt;SVM &amp;lt; &lt;strong&gt;400 mg&lt;/strong&gt; for &amp;ge; 60 ms&lt;/td&gt;
&lt;td&gt;Set for the wrist (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impact&lt;/td&gt;
&lt;td&gt;SVM &amp;ge; &lt;strong&gt;2000 mg&lt;/strong&gt; within 1 s of the free-fall ending&lt;/td&gt;
&lt;td&gt;Above what ordinary arm movement reaches without a free-fall before it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settle&lt;/td&gt;
&lt;td&gt;1 s ignored after the impact&lt;/td&gt;
&lt;td&gt;Bounces and rolling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stillness&lt;/td&gt;
&lt;td&gt;&amp;ge; 90 % of the next 3 s within &amp;plusmn;250 mg of 1 g&lt;/td&gt;
&lt;td&gt;The person is lying down, not walking away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orientation change&lt;/td&gt;
&lt;td&gt;Off (optional, e.g. &amp;ge; 30&amp;deg;)&lt;/td&gt;
&lt;td&gt;Can reject jumps, at the cost of missing some falls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cooldown&lt;/td&gt;
&lt;td&gt;10 s after a confirmed fall&lt;/td&gt;
&lt;td&gt;One fall, one alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The detector is O(1) per sample and uses integer arithmetic (an integer square root for SVM). Every threshold lives in a &lt;code&gt;fall_cfg_t&lt;/code&gt; struct, so it can be tuned without touching the logic. Free-fall is tracked in every state, so the record of the last free-fall is current when an impact arrives:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:5b5f69f3-34ae-47e6-8fd2-c9bc4e12fdf6:type=c_cpp&amp;text=fall_event_t%20fall_detect_feed%28fall_detect_t%20%2Afd%2C%20int16_t%20x_mg%2C%20int16_t%20y_mg%2C%20int16_t%20z_mg%29%0A%7B%0A%20%20%20%20const%20int32_t%20v%5B3%5D%20%3D%20%7B%20x_mg%2C%20y_mg%2C%20z_mg%20%7D%3B%0A%20%20%20%20const%20uint32_t%20sq%20%20%3D%20%28uint32_t%29%28v%5B0%5D%20%2A%20v%5B0%5D%29%20%2B%20%28uint32_t%29%28v%5B1%5D%20%2A%20v%5B1%5D%29%20%2B%20%28uint32_t%29%28v%5B2%5D%20%2A%20v%5B2%5D%29%3B%0A%20%20%20%20const%20uint16_t%20svm%20%3D%20fall_detect_isqrt%28sq%29%3B%0A%20%20%20%20...%0A%20%20%20%20%2F%2A%20Free-fall%20tracking%20runs%20in%20every%20state%2C%20so%20the%20memory%20is%20fresh%20when%20an%20impact%20arrives.%20%2A%2F%0A%20%20%20%20if%20%28svm%20%3C%20fd-%3Ecfg.freefall_mg%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20...%0A%20%20%20%20%20%20%20%20fd-%3Eff_run%2B%2B%3B%0A%20%20%20%20%7D%0A%20%20%20%20else%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20if%20%28fd-%3Eff_run%20%3E%3D%20fd-%3Eff_min_n%29%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20fd-%3Eff_last_len%20%20%3D%20fd-%3Eff_run%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20fd-%3Esince_ff_end%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20...%0A%20%20%20%20%20%20%20%20fd-%3Eff_run%20%3D%200%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20switch%20%28fd-%3Estate%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20case%20FD_IDLE%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20%28svm%20%3E%3D%20fd-%3Ecfg.impact_mg%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bool%20had_freefall%20%3D%20fd-%3Eff_last_len%20%21%3D%200%20%26%26%20fd-%3Esince_ff_end%20%3C%3D%20fd-%3Elookback_n%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20%28%21fd-%3Ecfg.require_freefall%20%7C%7C%20had_freefall%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fd-%3Eff_last_len%20%3D%200%3B%20%2F%2A%20one%20free-fall%20can%20explain%20only%20one%20impact%20%2A%2F%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fd-%3Estate%20%3D%20FD_SETTLE%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20FALL_EVENT_CANDIDATE%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20...]&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6ut6ae"&gt;Why 400 mg on the wrist&lt;/h3&gt;
&lt;p&gt;Most published fall thresholds come from sensors worn on the waist or chest. On the wrist, arm swing alone pulls SVM down to about 0.4&amp;ndash;0.6 g at the end of each swing. A 600 mg free-fall threshold fires on ordinary walking, so the threshold is set to 400 mg. The chip&amp;#39;s hardware free-fall at 350 mg is a sensible gate for the same reason.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6ut6af"&gt;Test results&lt;/h3&gt;
&lt;p&gt;The host tests (&lt;code&gt;firmware/tests/test_fall_detect.c&lt;/code&gt;) run synthetic 50 Hz traces through the detector on the PC:&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fall: 400 ms free-fall, 3.3 g impact, lying still&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Confirmed&lt;/strong&gt;, 400 ms free-fall, 3.27 g peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same, with the orientation check on (&amp;ge; 30&amp;deg;)&lt;/td&gt;
&lt;td&gt;Confirmed, 80&amp;deg; change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fall, then vigorous movement&lt;/td&gt;
&lt;td&gt;Rejected (the stillness check costs some real falls)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 s of walking with 2.3 g heel strikes&lt;/td&gt;
&lt;td&gt;No candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitting down hard (2.6 g), then still&lt;/td&gt;
&lt;td&gt;Ignored. Becomes a false alarm if free-fall isn&amp;#39;t required, which is why it&amp;#39;s on by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table slam with continued arm movement&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jump on the spot, then standing still&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;False alarm&lt;/strong&gt; with defaults; rejected with the orientation check on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40 ms free-fall before a hard impact&lt;/td&gt;
&lt;td&gt;Ignored (too short)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two falls 5 s apart&lt;/td&gt;
&lt;td&gt;Only the first reported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;The 72-hour run below also feeds every sample through the fall detector. It raised &lt;strong&gt;no candidates&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r6ut6ag"&gt;After a confirmed fall&lt;/h3&gt;
&lt;p&gt;A wrist-worn fall detector will produce some false alarms, so nothing goes to Home Assistant until the wearer has had a chance to cancel:&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x720/__key/communityserver-discussions-components-files/458/fig_2D00_04_2D00_fall_2D00_alert_2D00_flow.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15 s countdown:&lt;/strong&gt; the red LED blinks, the DRV2605L haptic motor buzzes and the piezo beeps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One press of SW1&lt;/strong&gt; cancels it. A &amp;quot;fall cancelled&amp;quot; record goes to the log.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Otherwise the alert latches:&lt;/strong&gt; the BTHome &lt;em&gt;Problem&lt;/em&gt; binary sensor turns on, and adverts speed up to every 100 ms for 30 s so Home Assistant sees it quickly. A &amp;quot;fall&amp;quot; record goes to flash with the peak g, the free-fall length and the stillness percentage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Another press of SW1&lt;/strong&gt; clears it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Home Assistant turns the alert into light. The automation below switches the bedroom WiZ bulb to full cool white when the Problem sensor turns on, and back to dim warm yellow when it clears:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/fig_2D00_05_2D00_ha_2D00_fall_2D00_alert_2D00_automation.png" /&gt;&lt;/p&gt;
&lt;p&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/20261026_5F00_005149.mp4:800:450]&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r728tah"&gt;Running both on the node&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;app_motion.c&lt;/code&gt; ties it together. Every 400 ms it drains the FIFO, converts the samples to mg, and feeds each one to both detectors:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:1c0f527c-76a2-4cd4-9298-2a9e1bcb78f6:type=c_cpp&amp;text=void%20app_motion_service%28void%29%0A%7B%0A%20%20%20%20lis2de12_raw_t%20raw%5BFIFO_MAX%5D%3B%0A%20%20%20%20uint8_t%20count%20%3D%200%3B%0A%20%20%20%20bool%20overrun%20%3D%20false%3B%0A%20%20%20%20if%20%28lis2de12_fifo_read%28raw%2C%20FIFO_MAX%2C%20%26count%2C%20%26overrun%29%20%21%3D%20NRF_SUCCESS%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%20%20%20%20for%20%28uint8_t%20i%20%3D%200%3B%20i%20%3C%20count%3B%20i%2B%2B%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20lis2de12_mg_t%20mg%3B%0A%20%20%20%20%20%20%20%20lis2de12_raw_to_mg%28%26raw%5Bi%5D%2C%20%26mg%29%3B%0A%20%20%20%20%20%20%20%20feed%28%26mg%29%3B%0A%20%20%20%20%7D%0A%7D%0A%0Astatic%20void%20feed%28const%20lis2de12_mg_t%20%2Amg%29%0A%7B%0A%20%20%20%20fall_event_t%20fe%20%3D%20fall_detect_feed%28%26s_fall%2C%20mg-%3Ex%2C%20mg-%3Ey%2C%20mg-%3Ez%29%3B%0A%20%20%20%20if%20%28fe%20%3D%3D%20FALL_EVENT_CONFIRMED%20%26%26%20s_countdown_ticks%20%3D%3D%200%20%26%26%20%21g_app.fall_alert%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20start_countdown%28%29%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20%28%21sleepq_feed%28%26s_sleep%2C%20mg-%3Ex%2C%20mg-%3Ey%2C%20mg-%3Ez%29%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2A%20A%20minute%20closed.%20%2A%2F%0A%20%20%20%20uint16_t%20n%20%3D%20sleepq_epoch_count%28%26s_sleep%29%3B%0A%20%20%20%20g_app.activity%20%3D%20sleepq_epoch_activity%28%26s_sleep%2C%20%28uint16_t%29%28n%20-%201%29%29%3B%0A%20%20%20%20uint16_t%20scored%20%3D%20sleepq_scored_count%28%26s_sleep%29%3B%0A%20%20%20%20if%20%28scored%20%3E%20s_reported%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20g_app.asleep%20%3D%20sleepq_epoch_is_asleep%28%26s_sleep%2C%20%28uint16_t%29%28scored%20-%201%29%29%3B%0A%20%20%20%20%20%20%20%20s_reported%20%20%20%3D%20scored%3B%0A%20%20%20%20%7D%0A%20%20%20%20app_ble_update%28%29%3B%0A%7D]&lt;/p&gt;
&lt;p&gt;The button goes through the nPM1300&amp;#39;s SHPHLD pin. A single press cancels or clears a fall alert. A double press marks bed time or getting up.&lt;/p&gt;
&lt;p&gt;In Home Assistant everything arrives over BTHome v2:&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BTHome object&lt;/th&gt;
&lt;th&gt;Shows in HA as&lt;/th&gt;
&lt;th&gt;Is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0x26 Problem&lt;/td&gt;
&lt;td&gt;Binary sensor&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fall alert&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x3D Count (1st)&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;Activity count, last minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x0F Generic binary&lt;/td&gt;
&lt;td&gt;Binary sensor&lt;/td&gt;
&lt;td&gt;Asleep (last scored minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x3D Count (2nd)&lt;/td&gt;
&lt;td&gt;Count&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Sleep minutes, last nigh&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/fig_2D00_06_2D00_ha_2D00_node_2D00_entities.png" /&gt;&lt;/p&gt;
&lt;p&gt;Home Assistant labels the SpO₂ value as Humidity. BTHome has no SpO₂ type, so the node sends SpO₂ as a percentage in the humidity object.&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r74pe9i"&gt;The 72-hour run&lt;/h2&gt;
&lt;p&gt;To see how the sleep and fall code behaves over several days, I ran 72 hours of wrist data through the same logic the firmware uses. It is 50 Hz wrist acceleration from the LIS2DE12 at &amp;plusmn;4 g, covering an ordinary routine: walking, desk work, meals, an evening on the sofa, bed at 23:00, and a few posture changes and twitches through the night. The data passes through the same code and the same bed and up logic as the firmware, fall detector included.&lt;/p&gt;
&lt;p&gt;This time I had a very special guest who helped me track the signals. My Grandmother who is the old schools and believes good sleep is essential for health.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/fig_2D00_07_2D00_three_2D00_day_2D00_run.png" /&gt;&lt;/p&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Night&lt;/th&gt;
&lt;th&gt;In bed&lt;/th&gt;
&lt;th&gt;Asleep&lt;/th&gt;
&lt;th&gt;Efficiency&lt;/th&gt;
&lt;th&gt;Fell asleep after&lt;/th&gt;
&lt;th&gt;WASO&lt;/th&gt;
&lt;th&gt;Awakenings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;480 min&lt;/td&gt;
&lt;td&gt;468 min&lt;/td&gt;
&lt;td&gt;97 %&lt;/td&gt;
&lt;td&gt;9 min&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;480 min&lt;/td&gt;
&lt;td&gt;465 min&lt;/td&gt;
&lt;td&gt;96 %&lt;/td&gt;
&lt;td&gt;11 min&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;480 min&lt;/td&gt;
&lt;td&gt;464 min&lt;/td&gt;
&lt;td&gt;96 %&lt;/td&gt;
&lt;td&gt;7 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;Time in bed was between 420 and 480 minutes across the three nights. To compare the nights in minutes rather than percentages, the table scales each night to a 480-minute window. Onset latency is 7&amp;ndash;11 minutes. The night-time posture changes show as spikes, and Cole&amp;ndash;Kripke rides through them, as designed. The fall detector raised no candidates in the 72 hours.&lt;/p&gt;
&lt;p&gt;The chart also shows the main limitation. From about 20:15 the evening on the sofa scores as asleep, and so do scattered quiet minutes at the desk: 502 of the 2868 daytime minutes. Cole&amp;ndash;Kripke measures stillness, not sleep. That&amp;#39;s why the night summary only uses the window between the two button presses, and why the live asleep flag shouldn&amp;#39;t be trusted on its own during the day.&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r775oej"&gt;Limits and next steps&lt;/h2&gt;
&lt;p&gt;These are the parts not yet validated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sleep:&lt;/strong&gt; the algorithm is good at detecting sleep and weaker at detecting quiet wakefulness. Lying awake still will count as sleep, so expect it to overestimate sleep somewhat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Falls:&lt;/strong&gt; the thresholds come from the literature and the wrist adjustments above. They haven&amp;#39;t been tuned on real falls with a person. The known weak spots are a jump followed by standing still, which looks like a fall, and a faller who gets up straight away, which is missed by the stillness check. The next step is labelled falls onto a mat, plus a day of ordinary wear, to tune on.&lt;/li&gt;
&lt;li&gt;The cancel countdown exists because of these limits. The alert should not be connected to an emergency service without it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mcetoc_1k5r775oek"&gt;Final notes&lt;/h2&gt;
&lt;p&gt;The LIS2DE12 does both jobs from one 50 Hz stream. The sleep counts follow ActiGraph&amp;#39;s published method and match its reference data. Sleep scoring runs in integer arithmetic on the MCU. The fall detector runs on a 400 mg free-fall threshold, tuned for the wrist. Every fall goes through a 15 s countdown before anyone is told.&lt;/p&gt;
&lt;p&gt;All the code is in the repository: the driver in &lt;code&gt;firmware/lib/lis2de12.[ch]&lt;/code&gt;, the algorithms in &lt;code&gt;firmware/lib/actcounts.[ch]&lt;/code&gt;, &lt;code&gt;sleepq.[ch]&lt;/code&gt; and &lt;code&gt;fall_detect.[ch]&lt;/code&gt;, the node integration in &lt;code&gt;firmware/vitarf_node/app_motion.c&lt;/code&gt;, and the host tests and the 72-hour run in &lt;code&gt;firmware/tests&lt;/code&gt; (&lt;code&gt;make test&lt;/code&gt;, &lt;code&gt;make three-day&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Repo Link:&amp;nbsp;&lt;a href="https://gitlab.com/arvindsa/vitarf-e14" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;https://gitlab.com/arvindsa/vitarf-e14&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r77pmll"&gt;Thank You&lt;/h2&gt;
&lt;p&gt;A Huge thank you for my Grandma for assisting with data collection and a Huge thanks to my Friend Ayush Who helped me with coding in of the Aceelerometer &amp;amp; Sleep Algorithms,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neishabouri A. et al., &amp;quot;Quantification of acceleration as activity counts in ActiGraph wearable&amp;quot;, &lt;em&gt;Scientific Reports&lt;/em&gt; 12, 11958 (2022). &lt;a class="url" href="https://doi.org/10.1038/s41598-022-16003-x" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;https://doi.org/10.1038/s41598-022-16003-x&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cole R.J., Kripke D.F., Gruen W., Mullaney D.J., Gillin J.C., &amp;quot;Automatic sleep/wake identification from wrist activity&amp;quot;, &lt;em&gt;Sleep&lt;/em&gt; 15(5):461&amp;ndash;469 (1992)&lt;/li&gt;
&lt;li&gt;Bourke A.K., O&amp;#39;Brien J.V., Lyons G.M., &amp;quot;Evaluation of a threshold-based tri-axial accelerometer fall detection algorithm&amp;quot;, &lt;em&gt;Gait &amp;amp; Posture&lt;/em&gt; 26(2):194&amp;ndash;199 (2007)&lt;/li&gt;
&lt;li&gt;Kangas M. et al., &amp;quot;Comparison of low-complexity fall detection algorithms for body attached accelerometers&amp;quot;, &lt;em&gt;Gait &amp;amp; Posture&lt;/em&gt; 28(2):285&amp;ndash;291 (2008)&lt;/li&gt;
&lt;li&gt;Sucerquia A., L&amp;oacute;pez J.D., Vargas-Bonilla J.F., &amp;quot;SisFall: A Fall and Movement Dataset&amp;quot;, &lt;em&gt;Sensors&lt;/em&gt; 17(1):198 (2017)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;actigraph/agcounts&lt;/code&gt; (reference counts implementation) and the &lt;code&gt;actigraph.sleepr&lt;/code&gt; R package (Cole&amp;ndash;Kripke reference)&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>RE: VitaRF - Part 9 - Sleep and Fall Detection</title><link>https://community.element14.com/thread/238797?ContentTypeID=1</link><pubDate>Tue, 27 Oct 2026 14:27:44 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:990fab96-1d66-46cd-909e-212d2078ef48</guid><dc:creator>Qbit</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/238797?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57388/vitarf---part-9---sleep-and-fall-detection/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Sad to hear that&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitaRF - Part 9 - Sleep and Fall Detection</title><link>https://community.element14.com/thread/238790?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 16:47:20 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:f13a1120-34d9-469c-ab15-7d1911c603db</guid><dc:creator>arvindsa</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/238790?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57388/vitarf---part-9---sleep-and-fall-detection/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;I made 3 devices, One for firmware development and two for data collection but one of them was having issues. If not, i would have more data points. And yes, the entire thing is very low power.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitaRF - Part 7 - Adding a Philips WiZ Smart Bulb to Home Assistant</title><link>https://community.element14.com/thread/238789?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 16:44:13 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:e6fc1855-a06f-4842-b26e-c6933f89c3d2</guid><dc:creator>arvindsa</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238789?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57337/vitarf---part-7---adding-a-philips-wiz-smart-bulb-to-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Thank you.&amp;nbsp;&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>VitaRF - Part 7 - Adding a Philips WiZ Smart Bulb to Home Assistant</title><link>https://community.element14.com/thread/57337?ContentTypeID=0</link><pubDate>Thu, 08 Oct 2026 05:14:12 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:285d27d8-e8fa-44f4-8586-46d6ca4a8b41</guid><dc:creator>arvindsa</dc:creator><slash:comments>4</slash:comments><comments>https://community.element14.com/thread/57337?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57337/vitarf---part-7---adding-a-philips-wiz-smart-bulb-to-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;&lt;strong&gt;Recap:&lt;/strong&gt; I am building a wireless vitals sensor node (heart rate, SpO₂, temperature) that reports over BLE into Home Assistant as a smart way to keep watch over our loved ones, built up in three phases &amp;mdash; DK bring-up, a custom miniaturized nRF52832 PCB, then a Matter over Thread proof of concept.&lt;/p&gt;
&lt;p&gt;Past Posts&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57254/vitarf---part-1---the-plan" data-e14adj="t"&gt;VitaRF - Part 1 - The Plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57279/vitarf---part-2---interface-max30102-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 2 - Interface MAX30102 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57291/vitarf---part-3---interfacing-max30208-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 3 - Interfacing MAX30208 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57293/vitarf---part-4---sending-sensor-data-to-home-assistant-via-ble-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 4 - Sending Sensor Data to Home Assistant via BLE on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57317/vitarf---part-5---matter-over-thread-on-nrf54l15-with-home-assistant" data-e14adj="t"&gt;VitaRF - Part 5 - Matter over Thread on nRF54L15 with Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57331/vitarf---part-6---nrf52-based-heart-rate-temperature-wearable-pcb-for-home-assistant" data-e14adj="t"&gt;VitaRF - Part 6 - An nRF52-Based Heart-Rate &amp;amp; Temperature Wearable PCB for Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The board is being manufactured at JLCPCB. JLCPCB has a few days of holidays, and I have no idea how that will affect the final delivery date, so while i wait, this post is a small detour on the Home Assistant side. If something concerning is detected by the sensors, especially at night, it should turn on the lights so that people don&amp;#39;t bump into things in panic.&lt;/p&gt;
&lt;h2 id="mcetoc_1k4cuatk40"&gt;The bulb&lt;/h2&gt;
&lt;p&gt;I found this &lt;a href="https://www.amazon.in/Philips-Connected-Dimmable-Compatible-Assistant/dp/B08669L2GQ" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;Philips Smart LED 10 W &amp;quot;Connected by WiZ&amp;quot;&lt;/a&gt; (there is no affiliate code or anything) for around 5 USD (INR 449). Standard bulb base, full colour plus warm-to-cool white, Wi-Fi + Bluetooth. WiZ seems to be their Branding for controls and the nice thing for me is that Home Assistant has a built-in WiZ integration that talks to the bulb locally over the LAN, no cloud account involved on the HA side.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:200px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x400/__key/communityserver-discussions-components-files/458/01_2D00_amazon_2D00_listing_2D00_philips_2D00_wiz_2D00_10w_2D00_b22.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:350px;max-width:400px;" alt=" " src="https://community.element14.com/resized-image/__size/800x700/__key/communityserver-discussions-components-files/458/02_2D00_wiz_2D00_bulb_2D00_box_2D00_and_2D00_powered_2D00_on.jpg" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k4cuchgk1"&gt;Setting it up in the WiZ app&lt;/h2&gt;
&lt;p&gt;The bulb needs to be put on your Wi-Fi first, and for that you need the WiZ Connected app from the Play Store. It was published by Signify Netherlands B.V. I was not sure if it was the right app, the logo seemed the same as on the box. A google search later, it turned out that Signify is the company behind Philips lighting (it used to be called Philips Lighting) and WiZ is their smart lighting brand, so it is the right app. It has a Google or Facebook sign-in, but I preferred the &amp;quot;Maybe later&amp;quot; option. Then, after a theme choice, it asks you to create a &amp;quot;home&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/03_2D00_wiz_2D00_app_2D00_install_2D00_signin_2D00_create_2D00_home.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;After the expected permission page, it searches over Bluetooth, and the bulb shows up as A60 with it&amp;#39;s MAC.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/04_2D00_wiz_2D00_app_2D00_permissions_2D00_search_2D00_bluetooth_2D00_pairing.jpg" /&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k4cuhhui2"&gt;A Misleading Dropdown for Wifi&lt;/h3&gt;
&lt;p&gt;Next is &amp;quot;Smart pairing&amp;quot;: it asks for your Wi-Fi name and password, and at the top there is a big 2.4 GHz banner. The bulb&amp;#39;s radio is 2.4 GHz only. The app helpfully lists the networks the phone can see in a dropdown, and that list included my 5 GHz network. I did not read the banner, i picked the 5 GHz one, typed the password, and hit Next.&lt;/p&gt;
&lt;p&gt;The bulb did not join, and started breathing red. It is a pretty clear &amp;quot;something went wrong&amp;quot; signal, to be fair, but the app does not tell you what it was. The app&amp;#39;s own result screen says &amp;quot;Wrong password&amp;quot; on the second entry. My password was right, so my guess is that this is just how the app reports &amp;quot;the bulb could not join this network&amp;quot;, and the actual reason was the 5 GHz band.&lt;/p&gt;
&lt;p&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/20261008_5F00_091209.mp4:640:360]&lt;/p&gt;
&lt;p&gt;After choosing the 2.4 GHz network instead, it went through, the app ran through a few optional steps (icon, name, power-on behaviour, fade time, all left at defaults) and finished with &amp;quot;Your device is successfully set up!&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/05_2D00_wiz_2D00_app_2D00_wifi_2D00_selection_2D00_error_2D00_and_2D00_done.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Apart from that dropdown, everything else about the app setup was straightforward.&lt;/p&gt;
&lt;h2 id="mcetoc_1k4culen93"&gt;Finding it from Home Assistant&lt;/h2&gt;
&lt;p&gt;I did this on the Home Assistant Docker instance that runs on my PC (for pure experimentation), and the PC is wired to a range extender. The bulb had joined the main router&amp;#39;s Wi-Fi, and Home Assistant could not see it. I tried adding it manually (Add integration, search &amp;quot;wiz&amp;quot;, leave the IP empty so it uses discovery) and nothing came of it:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;But then I made the bulb join the range extender&amp;#39;s Wi-Fi instead, and it got detected. It is the same problem i faced during the Matter setup, so i think it has something to do with my networking setup at home.&lt;/p&gt;
&lt;p&gt;Home Assistant picked it up on its own, &amp;quot;WiZ 64B3D2 (192.168.29.254)&amp;quot;, in the discovered devices. One click on Submit and it was in.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x720/__key/communityserver-discussions-components-files/458/06_2D00_ha_2D00_add_2D00_wiz_2D00_brand_2D00_ip_2D00_and_2D00_discovered.png" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k4cun0na4"&gt;It works&lt;/h2&gt;
&lt;p&gt;The device page shows it as &lt;code&gt;SHRGB by WiZ&lt;/code&gt;, firmware 1.34.0, hardware &lt;code&gt;ESP25 01&lt;/code&gt;, MAC &lt;code&gt;98:77:D5:64:B3:D2&lt;/code&gt;. It shows up as Hue1, in the Bedroom. There is an on/off control, an &amp;quot;Effect speed&amp;quot; slider, and a Power sensor which just says &amp;quot;Unknown&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;width:auto;" alt=" " src="https://community.element14.com/resized-image/__size/0x900/__key/communityserver-discussions-components-files/458/07_2D00_ha_2D00_wiz_2D00_bulb_2D00_device_2D00_page.png" /&gt;&lt;/p&gt;
&lt;p&gt;And here it is being controlled from Home Assistant:&lt;/p&gt;
&lt;p&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/20261008_5F00_093856.mp4:800:450]&lt;/p&gt;
&lt;p&gt;The Home Assistant side is documented here: &lt;a href="https://www.home-assistant.io/integrations/wiz/" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;WiZ integration&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="mcetoc_1k4cuptdo5"&gt;Final notes&lt;/h2&gt;
&lt;p&gt;The bulb is on my Home Assistant, and i can switch it on and off and change its colour from the dashboard. Time for some automation fun.&lt;/p&gt;
&lt;h2 id="mcetoc_1k4d0aigi0"&gt;Fun 1 - Music Sync&lt;/h2&gt;
&lt;p&gt;Using the Mobile app as an Microphone, It can visualize the song on the Bulb. There was no lag, but I felt the transitions were sometime choppy.&lt;/p&gt;
&lt;p&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/20261008_5F00_110502.mp4:800:450]&lt;/p&gt;</description></item><item><title>VitaRF - Part 6 -  NRF52-Based Heart-Rate &amp; Temperature Wearable PCB for Home Assistant</title><link>https://community.element14.com/thread/57331?ContentTypeID=0</link><pubDate>Tue, 06 Oct 2026 07:19:49 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:c95186ad-2ca3-4729-aba1-d4958e7b65e1</guid><dc:creator>arvindsa</dc:creator><slash:comments>8</slash:comments><comments>https://community.element14.com/thread/57331?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57331/vitarf---part-6---nrf52-based-heart-rate-temperature-wearable-pcb-for-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;&lt;strong&gt;Recap:&lt;/strong&gt; I am building a wireless vitals sensor node (heart rate, SpO₂, temperature) that reports over BLE into Home Assistant as a smart way to keep watch over our loved ones, built up in three phases &amp;mdash; DK bring-up, a custom miniaturized nRF52832 PCB, then a Matter over Thread proof of concept.&lt;/p&gt;
&lt;p&gt;Past Posts&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57254/vitarf---part-1---the-plan" data-e14adj="t"&gt;VitaRF - Part 1 - The Plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57279/vitarf---part-2---interface-max30102-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 2 - Interface MAX30102 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57291/vitarf---part-3---interfacing-max30208-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 3 - Interfacing MAX30208 on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57293/vitarf---part-4---sending-sensor-data-to-home-assistant-via-ble-on-nrf52" data-e14adj="t"&gt;VitaRF - Part 4 - Sending Sensor Data to Home Assistant via BLE on NRF52&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57317/vitarf---part-5---matter-over-thread-on-nrf54l15-with-home-assistant" data-e14adj="t"&gt;VitaRF - Part 5 - Matter over Thread on nRF54L15 with Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the last post I confessed that I had put off the PCB for &amp;quot;just one day&amp;quot; to play with the nRF54L15. Well, the day is over, and the responsible side of my brain finally won. This post is about the Phase 2 board - the schematic and the layout.&lt;/p&gt;
&lt;h2 id="mcetoc_1k4803fmp0"&gt;Why a custom board at all&lt;/h2&gt;
&lt;p&gt;The nRF52832 DK works great on a desk, but nobody is going to wear a DK on their wrist. The goal for Phase 2 is a small, low-cost, low-power node that can be strapped on and charged without fuss. To keep things simple, there will be only one board design. One will be populated with the MAX30208 (temperature) and another with the MAXREFDES117 (HR/SpO₂).&lt;/p&gt;
&lt;h2 id="mcetoc_1k4803fmp1"&gt;What&amp;#39;s on it&lt;/h2&gt;
&lt;figure class="table-figure"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Why it&amp;#39;s there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MCU&lt;/td&gt;
&lt;td&gt;RedBear MB-N2 (nRF52832 module)&lt;/td&gt;
&lt;td&gt;BLE, the same chip as the DK I have been using&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PMIC&lt;/td&gt;
&lt;td&gt;Nordic nPM1300&lt;/td&gt;
&lt;td&gt;Li-Po charger, fuel gauge, 2&amp;times; buck, 2&amp;times; load switch, LED driver, ship mode - all in one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accelerometer&lt;/td&gt;
&lt;td&gt;ST LIS2DE12&lt;/td&gt;
&lt;td&gt;Fall detection on INT1, motion (rolling over in sleep) on INT2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haptics&lt;/td&gt;
&lt;td&gt;TI DRV2605L&lt;/td&gt;
&lt;td&gt;Vibration alerts on a coin motor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash&lt;/td&gt;
&lt;td&gt;Winbond W25Q128JVS (16 MB)&lt;/td&gt;
&lt;td&gt;Logging readings when Home Assistant is out of range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buzzer&lt;/td&gt;
&lt;td&gt;Murata PKMCS0909E4000-R1 piezo&lt;/td&gt;
&lt;td&gt;Audible alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status LED&lt;/td&gt;
&lt;td&gt;Cree CLMVC-FKC RGB&lt;/td&gt;
&lt;td&gt;Driven by the nPM1300&amp;#39;s LED sinks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp sensor&lt;/td&gt;
&lt;td&gt;MAX30208 sponsor flex on a Molex 505110-0692 FPC connector&lt;/td&gt;
&lt;td&gt;Temp units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR/SpO₂&lt;/td&gt;
&lt;td&gt;MAXREFDES117 on a 6-pad ribbon solder footprint&lt;/td&gt;
&lt;td&gt;PPG units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charging&lt;/td&gt;
&lt;td&gt;2-pin magnetic pogo connector&lt;/td&gt;
&lt;td&gt;No USB-C port to keep sealed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug&lt;/td&gt;
&lt;td&gt;SWD and UART on test points&lt;/td&gt;
&lt;td&gt;No header on the board; pogo pins or soldered wires for programming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/figure&gt;
&lt;p&gt;Yes, the list grew. The original plan was &amp;quot;MCU + sensor + battery&amp;quot;. Then the accelerometer came in for fall detection, and once there is fall detection you want to tell the wearer something happened - hence the haptic motor and the buzzer. And once you have readings you don&amp;#39;t want to lose them when the phone or Home Assistant is out of range - hence the flash. Classic feature creep, but every part has a reason I can defend. And I did get an idea from&amp;nbsp;[mention:782d719f1d8c4ec989f5abf8fb4e62e6:e9ed411860ed4f2ba0265705b8793d05]&amp;nbsp;, Thanks to Him btw&amp;nbsp;, which I am working on integrating. Running into a lot of problems, but I am determined to make it work.&lt;/p&gt;
&lt;p&gt;The schematic is split into three hierarchical sheets - Power, MCU and Sensors - and the top level just wires them together: the shared I2C bus, the interrupt lines, and the two switched sensor rails.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/01_2D00_crop_2D00_top_2D00_hierarchy.png" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k4804db02"&gt;Why the MB-N2 and not a bare nRF52832&lt;/h2&gt;
&lt;p&gt;I have used bare-chip designs quite a lot in my projects recently, and one of the downsides of using a bare chip on a 1.6 mm 2-layer board is that the 50 &amp;Omega; feed line has to be about 3 mm wide. Which means i lose a lot of PCB space. So I decided to use a module, and I happen to have a lot of RedBear MB-N2 modules lying around. The &lt;a href="https://www.kickstarter.com/projects/redbearinc/bluetooth-5-ready-ble-module-nano-2-and-blend-2/" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;RedBear MB-N2&lt;/a&gt; is a small nRF52832 module that already has the RF matching, the antenna and the 32 MHz crystal on board, it is pre-certified, and it has castellated pads I can hand-solder. The company is no longer operational, but niche projects like this are the perfect use case to reduce my inventory of obsolete products.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/02_2D00_mb_2D00_n2.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;A few things I still had to add around it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;32.768 kHz crystal (Y1, with 12 pF load caps). The MB-N2 does not have a 32 kHz crystal on board, and it is needed for low-power operation.&lt;/li&gt;
&lt;li&gt;DC/DC inductors (L3 10 &amp;micro;H + L4 15 nH, C23 1 &amp;micro;F). The nRF52832 has an internal DC/DC regulator that is a lot more efficient than its LDO, but it needs these external parts. The values match RedBear&amp;#39;s own Blend 2 reference.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:550px;max-width:700px;" alt=" " src="https://community.element14.com/resized-image/__size/1400x1100/__key/communityserver-discussions-components-files/458/03_2D00_crop_2D00_mcu_2D00_mbn2.png" /&gt;&lt;/p&gt;
&lt;p&gt;Everything else on the MCU sheet is fairly standard. The I2C pull-ups (R2/R3, 4.7k) are the only pull-ups on the whole bus, R7 pulls up the MAXREFDES117&amp;#39;s open-drain interrupt, and the piezo is driven differentially from two GPIOs (P0.30/P0.31) through 100 &amp;Omega; resistors - one pin goes high while the other goes low, so the piezo sees twice the swing of a single GPIO, about 6.6 V peak-to-peak from a 3.3 V rail. Louder, for free (Nordic&amp;#39;s &lt;a href="https://devzone.nordicsemi.com/f/nordic-q-a/64264/driving-a-piezo-buzzer-using-nrf52840" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;DevZone&lt;/a&gt; calls this a full H-bridge without the external parts). The SWD and reset lines only go to test points.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/04_2D00_crop_2D00_mcu_2D00_pullups_2D00_buzzer.png" /&gt;&lt;/p&gt;
&lt;p&gt;The flash is a W25Q128 on SPI with 10k pull-ups on /CS, /WP and /HOLD, so it stays deselected and writable while the nRF is in reset:&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/05_2D00_crop_2D00_mcu_2D00_flash.png" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k480a1el3"&gt;Power: one chip instead of three&lt;/h2&gt;
&lt;p&gt;The power design kept changing till I actually started the schematic. I started with an all-ADI discrete stack to match the sponsor kit (MAX1555 charger + MAX17048 fuel gauge + MAX38640A buck), then swapped the charger to a TI BQ24074 because I have used it before. Then it hit me: the whole project is Nordic anyway, so why not Nordic&amp;#39;s own &lt;a href="https://www.nordicsemi.com/Products/nPM1300" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;nPM1300&lt;/a&gt;? It folds the charger, fuel gauge, two buck regulators, two load switches, an LED driver and a ship-mode power button into one QFN32. One part, one set of decoupling values to get right instead of three. Nordic&amp;#39;s Video at&amp;nbsp;&lt;a href="https://www.youtube.com/watch?v=2vBOIpyCltY" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;https://www.youtube.com/watch?v=2vBOIpyCltY&lt;/a&gt;&amp;nbsp;is quite helpful to understand what nPM1300 can do.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:400px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x800/__key/communityserver-discussions-components-files/458/06_2D00_power_2D00_tree.png" /&gt;&lt;/p&gt;
&lt;p&gt;Here is how it looks in the schematic. The input side first:&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/07_2D00_crop_2D00_power_2D00_input.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;J1 (pogo) brings in VBUS, with just a 1 &amp;micro;F cap (C14). CC1/CC2 are left unconnected, there is no USB here.&lt;/li&gt;
&lt;li&gt;J3 is the Li-Po, straight onto VBAT with a 2.2 &amp;micro;F cap. NTC is tied to GND, since I am using a protected Li-Po pouch cell without an NTC (firmware has to disable NTC monitoring, or the charger refuses to charge).&lt;/li&gt;
&lt;li&gt;VSET1 to GND means BUCK1 stays off at power-up. VSET2 through R1 (390k) means BUCK2 comes up at 3.3 V on its own, before any firmware has run. That is important: the nRF52832 is powered &lt;em&gt;by&lt;/em&gt; this buck, so it can&amp;#39;t be firmware that turns it on.&lt;/li&gt;
&lt;li&gt;SW1 (It stands for the Switched Node not the Switch, I made this mistake couple of times) on SHPHLD is the power button. It wakes the device from ship mode (the nPM1300&amp;#39;s &amp;quot;practically off&amp;quot; state, for storage and shipping). Once running, a short press is just an interrupt for firmware to use, and a long press resets.&lt;/li&gt;
&lt;li&gt;The two load switch inputs (LSIN1/2) and VDDIO all come from the 3.3 V rail, so the nPM1300&amp;#39;s I2C runs at the same level as everything else on the bus.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the output side:&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/08_2D00_crop_2D00_power_2D00_bucks.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VSYS is the nPM1300&amp;#39;s internal system rail, with 3&amp;times; 10 &amp;micro;F. It also feeds PVDD, the bucks&amp;#39; input.&lt;/li&gt;
&lt;li&gt;BUCK1: SW1 &amp;rarr; L2 2.2 &amp;micro;H &amp;rarr; AUX_3V3 (spare, off). &lt;strong&gt;BUCK2:&lt;/strong&gt; SW2 &amp;rarr; L1 2.2 &amp;micro;H &amp;rarr; +3.3 V, the main rail.&lt;/li&gt;
&lt;li&gt;PVSS1/PVSS2 are the bucks&amp;#39; power grounds. They go to GND through net ties NT1/NT2, the way Nordic&amp;#39;s reference layout does it. The net tie decides &lt;em&gt;where&lt;/em&gt; the noisy switching ground joins the main ground - right next to the PMIC - instead of letting it wander through the pour under the MCU.&lt;/li&gt;
&lt;li&gt;**LSOUT1/2 are the switched sensor rails, each with its own 10 &amp;micro;F and a test point.&lt;/li&gt;
&lt;li&gt;LED0/1/2 sink the RGB LED&amp;#39;s cathodes; the common anode is on VSYS.&lt;/li&gt;
&lt;li&gt;GPIO0 is the PMIC&amp;#39;s interrupt line to the nRF (charger events, button press).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Things I learnt from the nPM1300 datasheet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The two bucks are not identical.&lt;/strong&gt; BUCK1&amp;#39;s VSET resistor table only goes up to 2.7 V at power-up, BUCK2&amp;#39;s goes up to 3.3 V. So the 3.3 V rail had to be BUCK2 (R1 = 390 k&amp;Omega; 1 % on VSET2). If I had only looked at the block diagram, I would have wired 3.3 V to BUCK1 and found out at bring-up that it can&amp;#39;t get there. BUCK1 is a spare rail, off at power-up, and L2/C6 can be left unpopulated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buck inductors:&lt;/strong&gt; Murata DFE201610E-2R2M=P2, 2.2 &amp;micro;H, 140 m&amp;Omega;, 1.7 A. The datasheet asks for DCR &amp;le; 400 m&amp;Omega; and Isat &amp;gt; 350 mA, so there is plenty of margin. One catch: the KiCad stock footprint I used is for the DFE201610&lt;strong&gt;P&lt;/strong&gt;. Same body size, but I need to check the land pattern against the DFE201610&lt;strong&gt;E&lt;/strong&gt; drawing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VSYS is not regulated.&lt;/strong&gt; The datasheet says it plainly: VBUS &amp;quot;supplies VSYS but does not regulate its voltage&amp;quot;. On battery, VSYS &amp;asymp; VBAT. While charging, it sits close to VBUS, up to ~5.5 V. This is new to me, because the BQ24074 which I am used to does the opposite: when input power is present, its power-path output (OUT) is regulated to 4.4 V (&lt;a href="https://www.ti.com/lit/ds/symlink/bq24074.pdf" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;datasheet&lt;/a&gt;, Device Comparison Table), so whatever hangs off it never sees more than that even with a 5 V charger plugged in. The nPM1300 just passes VBUS through. That matters for anything I hang off VSYS, and it is why the 10 &amp;micro;F caps on VSYS/VBUS are 16&amp;ndash;25 V parts, to limit the DC-bias derating.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The sensors get switched rails.&lt;/strong&gt; LOADSW1 feeds &lt;code&gt;+3V3_PPG&lt;/code&gt; (MAXREFDES117) and LOADSW2 feeds &lt;code&gt;+3V3_TEMP&lt;/code&gt; (MAX30208), so firmware can cut a sensor&amp;#39;s power completely between readings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The LED driver has no PWM.&lt;/strong&gt; I assumed &amp;quot;LED driver&amp;quot; meant brightness control. It is strictly on/off at a fixed ~5 mA per channel. So no smooth colour fades, just 8 fixed colours and slow blinking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I swapped the USB-C port for a &lt;strong&gt;2-pin magnetic pogo connector&lt;/strong&gt;. On a wearable that will be charged every day, a magnetic connector is much nicer than fiddling with a port, and there is no opening to keep dust and sweat out of. The trade-off is that without the CC lines, the nPM1300&amp;#39;s input current limit stays at its 100 mA default until firmware raises it. So charging is slow until the firmware is written.&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:350px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x700/__key/communityserver-discussions-components-files/458/09_2D00_crop_2D00_sensors.png" /&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k480k7rd4"&gt;The sensor sheet&lt;/h2&gt;
&lt;p&gt;The LIS2DE12 accelerometer sits on the shared I2C bus, with CS tied high to select I2C mode and SA0 high for address 0x19. INT1 and INT2 go to separate GPIOs, because free-fall detection and general motion detection need different interrupt logic, and the chip can run both at once on separate pins.&lt;/p&gt;
&lt;p&gt;The two sensor connectors are just the bus, power and ground. &lt;strong&gt;J4&lt;/strong&gt; is the MAXREFDES117 ribbon (GND, INT, SDA, SCL, +3V3_PPG), and &lt;strong&gt;J2&lt;/strong&gt; is the FPC connector for the MAX30208 sponsor flex (GND, SDA, SCL, +3V3_TEMP). Only one of them gets populated per board.&lt;/p&gt;
&lt;h2 id="mcetoc_1k480mjgn5"&gt;Layout&lt;/h2&gt;
&lt;p&gt;The layout split itself quite naturally: all the ICs on the top, all the connectors on the bottom. The MB-N2 takes the top half of the front, with the flash to its right. The nPM1300 and its two bucks sit in the bottom-left corner, close together so the switching loops stay small, with the buzzer above them and the power button and RGB LED in the middle. The DRV2605L and the LIS2DE12 are on the right.&lt;/p&gt;
&lt;p&gt;On the back: the magnetic pogo connector along the bottom edge, the battery (J3) and motor (J6) JST-SH connectors, the MAX30208 FPC connector (J2) and the MAXREFDES117 ribbon pads (J4) on the left, the two solder jumpers, and the test points.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/10_2D00_vitarf_2D00_node_2D00_rendered_2D00_front.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/11_2D00_vitarf_2D00_node_2D00_rendered_2D00_back.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;J4 HaCK&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;There is no Connector for the J4 MAXREFDES117 Connector. It is just few rectangular pads spaced in a way that I can strip an end of a Ribbon connector and solder it on directly, the other end I will split them and solder to the MAXREFDES117 Board. I use this hack quite a lot for prototype PCB or when I am doubtful of the connection orientation. Here is a pic of the hack from my last Implementation.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style="display:block;margin-left:auto;margin-right:auto;max-height:450px;max-width:800px;" alt=" " src="https://community.element14.com/resized-image/__size/1600x900/__key/communityserver-discussions-components-files/458/20261006_5F00_124215_2800_1_2900_.jpg" /&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k480ootb6"&gt;Test points&lt;/h3&gt;
&lt;p&gt;There are 16 test points (1.5 mm pads), on every rail, I2C, UART and SWD. There is no SWD header on this board, so programming is via T13 (SWDIO), T15 (SWDCLK), T17 (reset) and GND, with wires soldered on for the first flash.&lt;/p&gt;
&lt;h2 id="mcetoc_1k480ootb7"&gt;Code and Hardware Files&lt;/h2&gt;
&lt;p&gt;All the KiCad files (schematic, layout, project library) and the fabrication outputs (Gerbers, BOM, pick-and-place) are in the &lt;a href="https://gitlab.com/arvindsa/vitarf-e14/-/tree/main/hardware/vitarf-node" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;hardware/vitarf-node&lt;/a&gt; folder of the project repo, along with the firmware from the earlier posts: &lt;a class="url" href="https://gitlab.com/arvindsa/vitarf-e14" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;https://gitlab.com/arvindsa/vitarf-e14&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k480ootb8"&gt;Final Notes&lt;/h2&gt;
&lt;p&gt;The schematic is done and reviewed, and the layout is routed with the DRC errors cleared. The most valuable part of this phase was the review, not the routing: two regulators shorted together, a split I2C net, and a buzzer that would never have buzzed, all caught on paper. Off to JLCPCB it goes.&lt;/p&gt;</description></item><item><title>RE: VitaRF - Part 6 -  NRF52-Based Heart-Rate &amp; Temperature Wearable PCB for Home Assistant</title><link>https://community.element14.com/thread/238788?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 16:43:32 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:c5181b00-3bef-4898-8c3a-897e72d26733</guid><dc:creator>arvindsa</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238788?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57331/vitarf---part-6---nrf52-based-heart-rate-temperature-wearable-pcb-for-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Thank you.&amp;nbsp; Yes I have used PCBway in past. But these days I default to using JLCPCB.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: EdgeCare: phase 3 - bracelet with two dry electrodes placed on the skin for the dielectric profile and more</title><link>https://community.element14.com/thread/238785?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 12:46:08 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:43cc303a-43c0-48a0-80e5-8501e7f7ba7d</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238785?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57334/edgecare-phase-3---bracelet-with-two-dry-electrodes-placed-on-the-skin-for-the-dielectric-profile-and-more/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice update on the project &lt;a href="https://community.element14.com/members/mihaita802003"&gt;mihaita802003&lt;/a&gt;&amp;nbsp;! But if you used slightly bigger images it would be better.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>EdgeCare: phase 3 - bracelet with two dry electrodes placed on the skin for the dielectric profile and more</title><link>https://community.element14.com/thread/57334?ContentTypeID=0</link><pubDate>Tue, 06 Oct 2026 20:42:56 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:9b684a6a-89af-4c40-bd39-094f7bdbd486</guid><dc:creator>mihaita802003</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/57334?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57334/edgecare-phase-3---bracelet-with-two-dry-electrodes-placed-on-the-skin-for-the-dielectric-profile-and-more/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57261/edgecare-phase-1---setup-ide-mcu-and-ble" data-e14adj="t"&gt;EdgeCare: phase 1 - setup IDE, MCU and BLE - element14 Community&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57283/edgecare-phase-2---setup-i2c-read-max30100-and-create-vi-in-labview" data-e14adj="t"&gt;EdgeCare: phase 2 - setup I2C, read MAX30100 and create VI in LabView - element14 Community&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;EdgeCare&amp;nbsp;will be a&amp;nbsp;Multi-Sensor Metabolic &amp;amp; Autonomic Datalogging Wearable with LabVIEW R&amp;amp;D Analytics.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;***********************************************************************************************************************************************************&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;OBS: at this moment, the sensors sent by element14 to me for this contest are blocked in custom because an issue with my EORI code.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I will continue to work on this project using MAX30100 and DS18B20 temperature sensor.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;These sensors will be replaced if the competition package reaches me in time.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;***********************************************************************************************************************************************************&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;In this post I will present the following implementations:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;A. Determination of body hydration using the dielectric profile (using GSR, Tau and Temp);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;B.&amp;nbsp;Cardiorespiratory efficiency and tissue perfusion (using HR and SpO2);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;C.&amp;nbsp;The body&amp;#39;s thermoregulatory response (using Temp + GSR);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;A. Determination of body hydration using the dielectric profile (using GSR, Tau and Temp);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;The circuit used to determine the dielectric profile for skin and to show the hydratation indes on the body can be one of the following solutions:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- a simple one with two resistors;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- with a specialized circuit, like AD5933 or MAX30009 (future implementation/upgrade).&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;I used first option, with two resistors and two ports from ESP32C6, like in the schematic diagram:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:208px;max-width:248px;" alt=" " height="208" src="https://community.element14.com/resized-image/__size/496x416/__key/communityserver-discussions-components-files/458/Schema-electrozi.png" width="248" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;OBS: the circuit used to read the braceled dry electrodes are powered borm an power bank to avoid interferenced induced by the switching power supply and to have a total separation between&amp;nbsp;&lt;/span&gt;&lt;span&gt;sensors and PC with Labview.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;All the calculation are done in LabView.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Parameter used:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- GSR&amp;nbsp;(Rskin in Kohm) - measure the DC resistance&amp;nbsp;of the skin; It is particularly influenced by surface moisture, perspiration and electrolytes.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- Skin capacity (Cskin) -&amp;nbsp;It reflects the dielectric properties of deep cellular tissue (dermis and living epidermis);&amp;nbsp;It depends directly on the bound and free water within the cells and in the interstitial space.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- Tau (Rskin x Cskin in us) - It measures the response speed to a transient pulse;&amp;nbsp;It measures the rate at which the skin&amp;#39;s dielectric capacitance charges and discharges in response to a pulse applied by the ESP32-C6.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;The parameters Temp (using DS10B20 one-wire temp sensor with metalic capsule), GSR and Tau&amp;nbsp;(using the bracelet two electrodes) are&amp;nbsp;read by the ESP32C6&amp;nbsp; (see the photos below) and sent to LabVIEW for processing and display:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:221px;max-width:166px;" alt=" " height="221" src="https://community.element14.com/resized-image/__size/332x442/__key/communityserver-discussions-components-files/458/Bracelet-on-hand.jpg" width="166" /&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;img style="max-height:220px;max-width:166px;" alt=" " src="https://community.element14.com/resized-image/__size/332x440/__key/communityserver-discussions-components-files/458/Circuit-board.jpg" /&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;img style="max-height:220px;max-width:166px;" alt=" " src="https://community.element14.com/resized-image/__size/332x440/__key/communityserver-discussions-components-files/458/DS18B20-on-hand.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Algorithm: the processing is carried out in four main stages in LabVIEW (within the Formula Node):&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Step 1:&lt;/strong&gt;&amp;nbsp;Thermal Compensation (DS18B20 Sensor)&lt;br /&gt;Skin temperature alters electrical resistance through vasodilation/vasoconstriction. To eliminate temperature-induced errors:&lt;/span&gt;&lt;/p&gt;
&lt;div class="math-block"&gt;Rcorr = GSR x (1 + alpha x (Tmasurat - 33.0))&lt;/div&gt;
&lt;div class="math-block"&gt;(where alpha = 0.02,&amp;nbsp;meaning a 2% correction per degree Celsius relative to the 33.0&amp;deg;C reference).&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;strong&gt;Step 2&lt;/strong&gt;:&amp;nbsp;Extraction of Dielectric Capacitance Cskin&lt;/div&gt;
&lt;div class="math-block"&gt;By mathematically separating the time constant tau and the corrected resistance Rcorr:&lt;/div&gt;
&lt;div class="math-block"&gt;Cskin (nF) = Tau (us) / Rcorr ()&lt;/div&gt;
&lt;div class="math-block"&gt;OBS:&amp;nbsp;The actual physical values ​​for the equivalent skin capacitance (Cskin) obtained using dry electrodes on the epidermis fall within the range of 0.2 nF &amp;ndash; 5.0 nF (200 pF &amp;ndash; 5000 pF),&lt;/div&gt;
&lt;div class="math-block"&gt;depending on the hydration state, electrode surface area, and contact pressure.&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Normalization and Calculation of Hydration Index (0 - 100%)&lt;/div&gt;
&lt;div class="math-block"&gt;The obtained value Cskin_nF is linearly scaled between the threshold for very dry skin (Cmin = 0.5 nF) and that for optimal hydration (Cmax = 3.0 nF):&lt;/div&gt;
&lt;div class="math-block"&gt;Ih(%) = Clamp x [(Cskin - Cmin) / (Cmax - Cmin) X 100, 0, 100),&amp;nbsp;&lt;/div&gt;
&lt;div class="math-block"&gt;where Clamp is a limitation fucntion used to mantined an interval between 0 and 100&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;strong&gt;Step 4&lt;/strong&gt;:&amp;nbsp;Safety Mask and Status Classification (status_code)&lt;/div&gt;
&lt;div class="math-block"&gt;- Status 0 --&amp;gt; (Disconnected): used for GSR &amp;gt;= 9999 or Tau = 0; braceled/sensors are not in contact with skin;&lt;/div&gt;
&lt;div class="math-block"&gt;- Status 1 --&amp;gt; (Dehydrated / Dried): Ih &amp;lt; 30%;&lt;/div&gt;
&lt;div class="math-block"&gt;- Status 2 --&amp;gt; (Moderate hydratation): 30% &amp;lt;= Ih &amp;lt; 60%;&lt;/div&gt;
&lt;div class="math-block"&gt;- Status 3 --&amp;gt; (Hydrated / Optimal): Ih &amp;gt;= 60%;&lt;/div&gt;
&lt;div class="math-block"&gt;- Status 4 --&amp;gt; (Surface perspiration):&amp;nbsp;Detected when Rcorr &amp;lt; 40 k&amp;Omega; (presence of surface electrolytes, independent of the deep layer).&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;
&lt;table width="741" height="171"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;GSR value (Rskin)&lt;/th&gt;
&lt;th&gt;Skin capacity Cskin&lt;/th&gt;
&lt;th&gt;Fisiologin interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dehydrated / Dried&lt;/td&gt;
&lt;td&gt;&amp;nbsp;Big (&amp;gt; 200&amp;nbsp;k&amp;Omega;)&lt;/td&gt;
&lt;td&gt;Low (&amp;lt;0.8 nF)&lt;/td&gt;
&lt;td&gt;Dry stratum corneum, low cellular volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moderate hydratation&lt;/td&gt;
&lt;td&gt;&amp;nbsp;Medium (80 - 180&amp;nbsp;k&amp;Omega;)&lt;/td&gt;
&lt;td&gt;Medium (1.0 - 2.2 nF)&lt;/td&gt;
&lt;td&gt;Optimal dermal/epidermal water balance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hydrated / optimal&lt;/td&gt;
&lt;td&gt;Normal-low (50 - 100k&amp;Omega;)&lt;/td&gt;
&lt;td&gt;Big (2.2 - 3.5 nF)&lt;/td&gt;
&lt;td&gt;Well-hydrated interstitial tissue.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surface perspiration&lt;/td&gt;
&lt;td&gt;&amp;nbsp;Very low (&amp;lt; 40 k&amp;Omega;)&lt;/td&gt;
&lt;td&gt;Rapidly Changing&lt;/td&gt;
&lt;td&gt;Excess electrolytes on the surface (surface perspiration, not cellular hydration).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The function used in Labview for all above calculatiuon is this (in formula node):&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:eb2a7cc0-23ed-4420-8aab-11e32fcbbb15:type=text&amp;text=%2F%2F%20Intrari%3A%20gsr%2C%20tau%2C%20temp%0D%0A%2F%2F%20Iesiri%3A%20hydratation_index%2C%20c_skin_nF%2C%20status_code%0D%0A%0D%0Aif%20%28gsr%20%3E%3D%209999.0%20%7C%7C%20tau%20%3D%3D%200%29%20%7B%0D%0A%20%20%20%20c_skin_nF%20%3D%200.0%3B%0D%0A%20%20%20%20hydratation_index%20%3D%200.0%3B%0D%0A%20%20%20%20status_code%20%3D%200%3B%20%2F%2F%20Deconectat%0D%0A%7D%20else%20%7B%0D%0A%20%20%20%20%2F%2F%201.%20Compensare%20termica%20pentru%20GSR%20%28Referinta%3A%2033.0%20%C2%B0C%2C%20Coeficient%3A%202%25%20per%20%C2%B0C%29%0D%0A%20%20%20%20float%20t_ref%20%3D%2033.0%3B%0D%0A%20%20%20%20float%20alpha%20%3D%200.02%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20%2F%2F%20Corectam%20GSR-ul%20daca%20temperatura%20este%20valida%0D%0A%20%20%20%20float%20gsr_corr%20%3D%20gsr%3B%0D%0A%20%20%20%20if%20%28temp%20%3E%2025.0%20%26%26%20temp%20%3C%2042.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20gsr_corr%20%3D%20gsr%20%2A%20%281.0%20%2B%20alpha%20%2A%20%28temp%20-%20t_ref%29%29%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20%2F%2F%202.%20Calcul%20capacitate%20dielectrici%20compensata%0D%0A%20%20%20%20c_skin_nF%20%3D%20tau%20%2F%20gsr_corr%3B%0D%0A%20%20%20%20%2F%2F%203.%20Scalare%20Indice%20de%20Hidratare%20%280%20-%20100%25%29%0D%0A%20%20%20%20float%20c_min%20%3D%200.5%3B%20%2F%2F%20nF%0D%0A%20%20%20%20float%20c_max%20%3D%203.0%3B%20%2F%2F%20nF%0D%0A%20%20%20%20%0D%0A%20%20%20%20float%20raw_index%20%3D%20%28%28c_skin_nF%20-%20c_min%29%20%2F%20%28c_max%20-%20c_min%29%29%20%2A%20100.0%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20if%20%28raw_index%20%3C%200.0%29%20raw_index%20%3D%200.0%3B%0D%0A%20%20%20%20if%20%28raw_index%20%3E%20100.0%29%20raw_index%20%3D%20100.0%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20hydratation_index%20%3D%20raw_index%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%204.%20Clasificare%20stare%0D%0A%20%20%20%20if%20%28gsr_corr%20%3C%2040.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%204%3B%20%2F%2F%20Transpiratie%20%2F%20Hiperhidroza%0D%0A%20%20%20%20%7D%20else%20if%20%28hydratation_index%20%3E%3D%2060.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%203%3B%20%2F%2F%20Hidratat%20%2F%20Optim%0D%0A%20%20%20%20%7D%20else%20if%20%28hydratation_index%20%3E%3D%2030.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%202%3B%20%2F%2F%20Hidratare%20Moderata%0D%0A%20%20%20%20%7D%20else%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%201%3B%20%2F%2F%20Deshidratat%20%2F%20Uscat%0D%0A%20%20%20%20%7D%0D%0A%7D]&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;img style="max-height:172px;max-width:343px;" alt=" " height="172" src="https://community.element14.com/resized-image/__size/686x344/__key/communityserver-discussions-components-files/458/LabView_5F00_002.png" width="343" /&gt;&lt;/p&gt;
&lt;p&gt;Demo:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/Dielectric-profile.mp4:505:268]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OBS&lt;/strong&gt;:&amp;nbsp;The EdgeCare system utilizes the parallel RC equivalent model for epidermal impedance described by Geddes &amp;amp; Baker (1989) and Martinsen &amp;amp; Grimnes (2011).&lt;/p&gt;
&lt;p&gt;The determination of the dielectric capacitance (Cskin = Tau / R) and its scaling to derive the hydration index follow the dynamics of electrodermal activity (EDA) variation established by Boucsein (2012),&lt;/p&gt;
&lt;p&gt;with a thermal correction using an alpha coefficient of 0.02&amp;deg;C applied in accordance with Wenig &amp;amp; Martinsen (2000).&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;B.&amp;nbsp;Cardiorespiratory efficiency and tissue perfusion; apnea detection&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;This module assesses in real time how efficiently the cardiovascular system delivers oxygen to peripheral tissues and how the body reacts to stress or hypoxia.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;To achive this, an new parameter is introduced: PI (Tissue Perfusion Index).&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;How it is working: PI it represents the ratio between the pulsatile alternating component (AC, pulsating arterial blood) and the non-pulsatile continuous component (DC, tissue, bone, static venous blood)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;The formula used is this: PI = (AC / DC ) * 100%&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Interpretation:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;- PI &amp;gt;=1.0%&amp;nbsp; -&amp;nbsp;indicates good tissue perfusion in the finger;&lt;/p&gt;
&lt;p&gt;- PI &amp;lt; 0.5% - indicates peripheral vasoconstriction, cold fingers, or a weak signal.&lt;/p&gt;
&lt;p&gt;SpO2 and HR are both displayed on the same Waveform Chart with double Y-axes:&lt;/p&gt;
&lt;p&gt;- on the left side is displayed HW that monitors variations in ventricular rate (40 --180 BPM), in pink colour;&lt;/p&gt;
&lt;p&gt;-&amp;nbsp; on the right side is displayed SpO2 that mMonitors arterial oxygenation levels (70-100\%).&lt;/p&gt;
&lt;p&gt;Apnea detection: the algorithm runs inside the Formula Node block in LabVIEW and evaluates the conditions for an episode of obstructive or central sleep apnea.&lt;/p&gt;
&lt;p&gt;The algorithm and Labview implementation:&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:a96be0be-69c2-47c6-8f4e-be05c70da2e5:type=text&amp;text=%2F%2F%20Intrari%3A%20gsr%2C%20tau%2C%20temp%2C%20pi%2C%20spo2%0D%0A%2F%2F%20Iesiri%3A%20hydratation_index%2C%20c_skin_nF%2C%20status_code%2C%20low_perfusion%2C%20cardio_status%2C%20apnea_alert%0D%0A%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%201.%20EVALUARE%20PROFIL%20DIELECTRIC%20%28GSR%20%2B%20TAU%29%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%0D%0Aif%20%28gsr%20%3E%3D%209999.0%20%7C%7C%20tau%20%3D%3D%200%29%20%7B%0D%0A%20%20%20%20c_skin_nF%20%3D%200.0%3B%0D%0A%20%20%20%20hydratation_index%20%3D%200.0%3B%0D%0A%20%20%20%20status_code%20%3D%200%3B%20%2F%2F%20Deconectat%0D%0A%7D%20else%20%7B%0D%0A%20%20%20%20%2F%2F%201.%20Compensare%20termica%20pentru%20GSR%20%28Referinta%3A%2033.0%20%C2%B0C%2C%20Coeficient%3A%202%25%20per%20%C2%B0C%29%0D%0A%20%20%20%20float%20t_ref%20%3D%2033.0%3B%0D%0A%20%20%20%20float%20alpha%20%3D%200.02%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20%2F%2F%20Corectam%20GSR-ul%20daca%20temperatura%20este%20valida%0D%0A%20%20%20%20float%20gsr_corr%20%3D%20gsr%3B%0D%0A%20%20%20%20if%20%28temp%20%3E%2025.0%20%26%26%20temp%20%3C%2042.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20gsr_corr%20%3D%20gsr%20%2A%20%281.0%20%2B%20alpha%20%2A%20%28temp%20-%20t_ref%29%29%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20%2F%2F%202.%20Calcul%20capacitate%20dielectrici%20compensata%0D%0A%20%20%20%20c_skin_nF%20%3D%20tau%20%2F%20gsr_corr%3B%0D%0A%20%20%20%20%2F%2F%203.%20Scalare%20Indice%20de%20Hidratare%20%280%20-%20100%25%29%0D%0A%20%20%20%20float%20c_min%20%3D%200.5%3B%20%2F%2F%20nF%0D%0A%20%20%20%20float%20c_max%20%3D%203.0%3B%20%2F%2F%20nF%0D%0A%20%20%20%20%0D%0A%20%20%20%20float%20raw_index%20%3D%20%28%28c_skin_nF%20-%20c_min%29%20%2F%20%28c_max%20-%20c_min%29%29%20%2A%20100.0%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20if%20%28raw_index%20%3C%200.0%29%20raw_index%20%3D%200.0%3B%0D%0A%20%20%20%20if%20%28raw_index%20%3E%20100.0%29%20raw_index%20%3D%20100.0%3B%0D%0A%20%20%20%20%0D%0A%20%20%20%20hydratation_index%20%3D%20raw_index%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%204.%20Clasificare%20stare%0D%0A%20%20%20%20if%20%28gsr_corr%20%3C%2040.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%204%3B%20%2F%2F%20Transpiratie%20%2F%20Hiperhidroza%0D%0A%20%20%20%20%7D%20else%20if%20%28hydratation_index%20%3E%3D%2060.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%203%3B%20%2F%2F%20Hidratat%20%2F%20Optim%0D%0A%20%20%20%20%7D%20else%20if%20%28hydratation_index%20%3E%3D%2030.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%202%3B%20%2F%2F%20Hidratare%20Moderata%0D%0A%20%20%20%20%7D%20else%20%7B%0D%0A%20%20%20%20%20%20%20%20status_code%20%3D%201%3B%20%2F%2F%20Deshidratat%20%2F%20Uscat%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%202.%20EVALUARE%20INDICE%20DE%20PERFUZIE%20%28PI%29%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0Aint%20low_perfusion%20%3D%200%3B%0D%0Aif%20%28pi%20%3E%200.0%20%26%26%20pi%20%3C%200.5%29%20%7B%0D%0A%20%20%20%20low_perfusion%20%3D%201%3B%20%2F%2F%20Vasoconstruc%3Fie%20%2F%20Degete%20reci%0D%0A%7D%0D%0A%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%203.%20STARE%20CARDIORESPIRATORIE%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%200%3A%20Deconectat%20%7C%201%3A%20Normal%20%7C%202%3A%20Hipoxie%20%7C%203%3A%20Aritmie%2FAnormal%0D%0Aif%20%28spo2%20%3D%3D%200%20%7C%7C%20hr%20%3D%3D%200%29%20%7B%0D%0A%20%20%20%20cardio_status%20%3D%200%3B%0D%0A%7D%20else%20if%20%28spo2%20%3E%3D%2095.0%20%26%26%20hr%20%3E%3D%2050.0%20%26%26%20hr%20%3C%3D%20100.0%29%20%7B%0D%0A%20%20%20%20cardio_status%20%3D%201%3B%20%0D%0A%7D%20else%20if%20%28spo2%20%3C%2090.0%29%20%7B%0D%0A%20%20%20%20cardio_status%20%3D%202%3B%20%0D%0A%7D%20else%20%7B%0D%0A%20%20%20%20cardio_status%20%3D%203%3B%20%0D%0A%7D%0D%0A%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%204.%20ALGORITM%20ALERTA%20APNEE%20%C3%8EN%20SOMN%0D%0A%2F%2F%20Scadere%20SpO2%20%3C%3D%2092%25%20corelata%20cu%20micro-trezire%20%28HR%20%3E%3D%2085%20BPM%29%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0Aint%20apnea_alert%3B%0D%0Aif%20%28spo2%20%3E%200%20%26%26%20spo2%20%3C%3D%2092.0%20%26%26%20hr%20%3E%3D%2085.0%29%20%7B%0D%0A%20%20%20%20apnea_alert%20%3D%201%3B%20%2F%2F%20Alerta%20apnee%20%2F%20desaturare%0D%0A%7D%20else%20%7B%0D%0A%20%20%20%20apnea_alert%20%3D%200%3B%0D%0A%7D]&amp;nbsp;&amp;nbsp;&lt;img style="max-height:128px;max-width:261px;" alt=" " height="128" src="https://community.element14.com/resized-image/__size/522x256/__key/communityserver-discussions-components-files/458/LabView_5F00_003.png" width="261" /&gt;&lt;/p&gt;
&lt;p&gt;Working mechanism:&lt;/p&gt;
&lt;p&gt;- Respiratory pause (Apnea): During the cessation of breathing, blood oxygen levels begin to drop progressively;&lt;/p&gt;
&lt;p&gt;- Desaturation (SpO_2 &amp;lt;= 92\%): When the SpO_2 level falls below the critical threshold of 92\%, the body enters a state of mild hypoxia;&lt;/p&gt;
&lt;p&gt;- Micro-arousal / Sympathetic stress response (HR &amp;gt;= 85 BPM): The brain detects the lack of oxygen and activates the sympathetic nervous system, triggering a sudden increase in heart rate (compensatory tachycardia) to circulate the remaining oxygenated blood more rapidly;&lt;/p&gt;
&lt;p&gt;- Signal validation (SpO_2 &amp;gt; 0): Prevents false alarms from triggering when the sensor is removed from the finger (disconnected state).&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
Demo:&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/Cardio-profile.mp4:640:360]&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;&lt;/div&gt;
&lt;div class="math-block"&gt;
&lt;p&gt;&lt;strong&gt;OBS&lt;/strong&gt;:&amp;nbsp;The perfusion index (PI) was evaluated according to the methodology described by Allen [5], while the arterial desaturation thresholds for apnea follow the recommendations of Penzel et al. [7].&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;C. Detection of febrile conditions or heat shock.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The Temp + GSR (Body Thermoregulatory Response) module complements the EdgeCare biometric system by adding the component of metabolic and autonomic nervous system (ANS) assessment.&lt;/p&gt;
&lt;p&gt;If oximetry (HR, SpO_2, PI) measures cardiorespiratory function, the combination Temperature + GSR monitors heat stress and electrodermal reactivity.&lt;/p&gt;
&lt;p&gt;The role of parameters:&lt;br /&gt;a. Peripheral/Skin Temperature (Temp):&lt;br /&gt;- Febrile State / Hyperthermia (Temp &amp;gt; 37.5C): The body tries to eliminate excessive heat through peripheral vasodilatation and sweating.&lt;br /&gt;- Heat Shock / Hypothermia / Vasoconstriction (Temp &amp;lt; 35.0C): The body redirects blood from the periphery to the vital organs (fingers become cold, correlated with a massive drop in PI).&lt;br /&gt;b. Galvanic Skin Response (GSR / Electrodermal Conductance)&lt;br /&gt;- The eccrine sweat glands are innervated exclusively by the sympathetic nervous system.&lt;br /&gt;- In case of intense physical exertion, thermal shock, pain or emotional stress, sweat activity increases instantly.&lt;br /&gt;- Water and minerals in sweat remove skin resistance and increase GSR conductance.&lt;/p&gt;
&lt;p&gt;Code for this module (this code will be added in Formula node from Labview):&lt;/p&gt;
&lt;p&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:72248bef-4cad-4942-9cb3-62963f14cb00:type=text&amp;text=%2F%2F%20Detectie%20Soc%20Termic%20sau%20Stare%20Febrila%0Aif%20%28temp%20%3E%2038.0%29%20%7B%0A%20%20%20%20fever_alert%20%3D%201%3B%20%2F%2F%20Indicator%20Febr%C4%83a%2F%20Hipertermie%0A%7D%20else%20if%20%28temp%20%3C%2035.0%20%26%26%20pi%20%3C%200.5%29%20%7B%0A%20%20%20%20shock_alert%20%3D%201%3B%20%2F%2F%20Indicator%20Posibil%20Soc%20Termic%20%2F%20Hipotermie%0A%7D%20else%20%7B%0A%20%20%20%20fever_alert%20%3D%200%3B%0A%20%20%20%20shock_alert%20%3D%200%3B%0A%7D]&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;img style="max-height:159px;max-width:316px;" alt=" " height="159" src="https://community.element14.com/resized-image/__size/632x318/__key/communityserver-discussions-components-files/458/LabView_5F00_004.png" width="316" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;Demo:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/Fever-alert.mp4:640:360]&lt;/span&gt;&lt;span style="font-size:150%;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;OBS: to simulate fever in &amp;quot;The Body&amp;#39;s termoregulatory Response&amp;quot; area&amp;nbsp;I heated the temperature sensor a little above the trigger threshold of the alert.&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;span&gt;D. Estimated Metabolic Inflection Indicator.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;What is &amp;quot;Estimated Metabolic Inflection Indicator&amp;quot;: this is a multisensor fusion algorithm that evaluates, in real time, the direction of blood glucose fluctuation (rising, stable, or rapidly falling). Without requiring blood sampling or subcutaneous sensors, the method monitors the neurovascular and autonomic footprint that metabolic changes leave on the body.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;How is working: the algorithm relies on the involuntary reaction of the sympathetic nervous system (SNS) and the cardiovascular response to glucose fluctuations:&lt;br /&gt;- During rapid drops in blood glucose (risk of hypoglycemia): The body releases adrenaline, triggering an acute &amp;quot;fight-or-flight&amp;quot; response. This manifests as cold sweating (a sudden rise in skin conductance/GSR), compensatory tachycardia (increased heart rate/HR), and peripheral vasoconstriction (a drop in the perfusion index/PI).&lt;br /&gt;- During postprandial states (rise in blood glucose): The reaction is more gradual, characterized by incremental adjustments in skin temperature and GSR, without sudden adrenergic surges.&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;Code for this module (this code will be added in Formula node from Labview):&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:8d80ba7a-a4af-4b63-99cf-39f7d282563e:type=text&amp;text=%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%206.%20ALGORITM%20pentru%20Indicator%20Estimat%20de%20Inflexiune%20Metabolica%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0A%2F%2F%20Declarare%20variabile%20interne%20pentru%20stocare%20stare%20anterioara%20memorare%20esantion%0D%0Afloat%20gsr_prev%3B%0D%0Afloat%20hr_prev%3B%0D%0A%0D%0A%2F%2F%20Calculul%20variatiei%20rapide%20%28derivata%20discreta%29%0D%0Afloat%20delta_gsr%20%3D%20gsr%20-%20gsr_prev%3B%0D%0Afloat%20delta_hr%20%3D%20hr%20-%20hr_prev%3B%0D%0A%0D%0A%2F%2F%20Initializare%20scor%3A%200%20%3D%20Stabil%20%2F%20Homeostazie%0D%0Aglycemic_trend%20%3D%200%3B%20%0D%0Aglycemic_alert%20%3D%200%3B%0D%0A%0D%0A%2F%2F%20Verificare%20validitate%20date%20%28evita%20alarme%20false%20la%20deconectare%29%0D%0Aif%20%28spo2%20%3E%200.0%20%26%26%20hr%20%3E%200.0%29%20%7B%0D%0A%0D%0A%20%20%20%20%2F%2F%201.%20Trend%20de%20Scadere%20Rapida%20%2F%20Stres%20Metabolic%20Sympathetic%20%28Risc%20Hipoglicemic%29%0D%0A%20%20%20%20%2F%2F%20Caracterizat%20prin%3A%20crestere%20brusca%20GSR%20%28transpiratie%29%20%2B%20accelerare%20HR%20%2B%20PI%20scazut%20%28vasoconstructie%29%0D%0A%20%20%20%20if%20%28delta_gsr%20%3E%2015.0%20%26%26%20delta_hr%20%3E%203.0%20%26%26%20pi%20%3C%201.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20glycemic_trend%20%3D%20-1%3B%20%2F%2F%20Trend%20descrescator%20%28Scadere%20rapida%29%0D%0A%20%20%20%20%20%20%20%20glycemic_alert%20%3D%201%3B%20%20%2F%2F%20Declansare%20alerta%20vizuala%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20%0D%0A%20%20%20%20%2F%2F%202.%20Trend%20de%20Crestere%20%2F%20Raspuns%20Postprandial%0D%0A%20%20%20%20%2F%2F%20Caracterizat%20prin%3A%20crestere%20lenta%20Temp%2FGSR%20fara%20peak-uri%20sau%20fluctuatii%20majore%20de%20HR%0D%0A%20%20%20%20else%20if%20%28delta_gsr%20%3C%20-5.0%20%26%26%20temp%20%3E%2036.6%20%26%26%20delta_hr%20%3E%3D%20-2.0%20%26%26%20delta_hr%20%3C%3D%202.0%29%20%7B%0D%0A%20%20%20%20%20%20%20%20glycemic_trend%20%3D%201%3B%20%20%2F%2F%20Trend%20crescator%0D%0A%20%20%20%20%20%20%20%20glycemic_alert%20%3D%200%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20%0D%0A%20%20%20%20%2F%2F%203.%20Stare%20Stabila%20%2F%20Homeostazie%0D%0A%20%20%20%20else%20%7B%0D%0A%20%20%20%20%20%20%20%20glycemic_trend%20%3D%200%3B%20%20%2F%2F%20Stare%20neutra%20%2F%20stabila%0D%0A%20%20%20%20%20%20%20%20glycemic_alert%20%3D%200%3B%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Salvare%20valori%20curente%20pentru%20esantionul%20urmator%0D%0Agsr_prev%20%3D%20gsr%3B%0D%0Ahr_prev%20%3D%20hr%3B]&amp;nbsp; &amp;nbsp;&lt;img style="max-height:105px;max-width:206px;" alt=" " height="105" src="https://community.element14.com/resized-image/__size/412x210/__key/communityserver-discussions-components-files/458/LabView_5F00_005.png" width="206" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;The front panel in Labview (no video provided, see last note):&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;img style="max-height:167px;max-width:233px;" alt=" " height="167" src="https://community.element14.com/resized-image/__size/466x334/__key/communityserver-discussions-components-files/458/Glicemia.png" width="233" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;&lt;strong&gt;OBS:&amp;nbsp;&lt;/strong&gt;The metabolic trend monitoring method integrated into the EdgeCare system is based on the multisensory fusion hypothesis validated in the scientific literature.&lt;/p&gt;
&lt;p&gt;According to recent studies on autonomic responses to metabolic imbalances, a drop in glucose levels triggers acute sympathetic activation, manifested as rapid variations in electrodermal conductance (GSR) and peripheral hemodynamic adjustments derived from the PPG signal (HR, PI).&lt;/p&gt;
&lt;p&gt;The algorithm developed in LabVIEW utilizes these discrete metrics to classify the trend of variation without requiring invasive devices.&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt;&amp;nbsp;With this final feature, I aimed to create a device capable of indicating potential rises, falls, or periods of stability in estimated blood glucose levels. To generate this estimate, I utilized GSR, HR, PI, and temperature parameters. This device is not intended to replace a standard glucometer, as the goal is to provide a rough estimate of the trend rather than a precise blood glucose reading; consequently, it should be regarded as an experimental device.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;span style="text-decoration:underline;"&gt;this last module is purely theoretical and experimental; calibrating the algorithm requires a significant number of measurements taken at various times of the day, such as in the morning, after breakfast, before and after lunch, before and after dinner, and at night.&lt;/span&gt;&lt;br /&gt;&lt;span style="text-decoration:underline;"&gt;Therefore, this module will be updated once I have created the final hardware version: the wristband with the sensors and power supply installed.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="text-decoration:underline;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:150%;"&gt;Bibliography&lt;/span&gt;:&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="4,0,0" data-index-in-node="0"&gt;Geddes, L. A., &amp;amp; Baker, L. E. (1989).&lt;/b&gt; &lt;i data-path-to-node="4,0,0" data-index-in-node="38"&gt;Principles of Applied Biomedical Instrumentation&lt;/i&gt; (3rd ed.). John Wiley &amp;amp; Sons.&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="7,0,0" data-index-in-node="0"&gt;Martinsen, O. G., &amp;amp; Grimnes, S. (2011).&lt;/b&gt; &lt;i data-path-to-node="7,0,0" data-index-in-node="40"&gt;Bioimpedance and Bioelectricity Basics&lt;/i&gt; (3rd ed.). Academic Press.&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="10,0,0" data-index-in-node="0"&gt;Wenig, P. B., &amp;amp; Martinsen, O. G. (2000).&lt;/b&gt; &lt;i data-path-to-node="10,0,0" data-index-in-node="41"&gt;Thermal influence on skin bioimpedance measurements.&lt;/i&gt; Annals of Biomedical Engineering, 28(5), 582&amp;ndash;588.&lt;/p&gt;
&lt;p&gt;&lt;b data-path-to-node="7,1,0" data-index-in-node="0"&gt;- Boucsein, W. (2012).&lt;/b&gt; &lt;i data-path-to-node="7,1,0" data-index-in-node="21"&gt;Electrodermal Activity&lt;/i&gt; (2nd ed.). Springer Science &amp;amp; Business Media.&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="3,0,0" data-index-in-node="0"&gt;J. Allen&lt;/b&gt;, &lt;i data-path-to-node="3,0,0" data-index-in-node="10"&gt;&amp;bdquo;Photoplethysmography and its application in clinical physiological measurement&amp;rdquo;&lt;/i&gt;, &lt;b data-path-to-node="3,0,0" data-index-in-node="92"&gt;Physiological Measurement&lt;/b&gt;, vol. 28, no. 3, pp. R1&amp;ndash;R39, 2007.&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="3,2,0" data-index-in-node="0"&gt;MAXIM Integrated&lt;/b&gt;, &lt;i data-path-to-node="3,2,0" data-index-in-node="18"&gt;&amp;bdquo;MAX30100: Pulse Oximeter and Heart-Rate Sensor IC&amp;rdquo;&lt;/i&gt;, &lt;b data-path-to-node="3,2,0" data-index-in-node="71"&gt;Datasheet &amp;amp; Application Notes&lt;/b&gt;, Rev. 0, 2014.&lt;/p&gt;
&lt;p&gt;-&amp;nbsp;&lt;b data-path-to-node="6,0,0" data-index-in-node="0"&gt;T. Penzel, et al.&lt;/b&gt;, &lt;i data-path-to-node="6,0,0" data-index-in-node="19"&gt;&amp;bdquo;The apnea-hypopnea index: history, clinical use, and future potential&amp;rdquo;&lt;/i&gt;, &lt;b data-path-to-node="6,0,0" data-index-in-node="92"&gt;Journal of Clinical Sleep Medicine&lt;/b&gt;, 2012.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;</description></item><item><title>RE: VitaRF - Part 6 -  NRF52-Based Heart-Rate &amp; Temperature Wearable PCB for Home Assistant</title><link>https://community.element14.com/thread/238784?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 12:43:42 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:e4a03944-4cef-477c-8c43-29138b81e0af</guid><dc:creator>Qbit</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/238784?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57331/vitarf---part-6---nrf52-based-heart-rate-temperature-wearable-pcb-for-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice walkthrough on the PCB design &lt;a href="https://community.element14.com/members/arvindsa"&gt;arvindsa&lt;/a&gt;&amp;nbsp; &amp;nbsp;and the component selection you made! Did you also have used PCBWay?&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitaRF - Part 7 - Adding a Philips WiZ Smart Bulb to Home Assistant</title><link>https://community.element14.com/thread/238783?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 12:29:56 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:ad5f88e4-082e-4236-bbde-495f21090f62</guid><dc:creator>Qbit</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/238783?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57337/vitarf---part-7---adding-a-philips-wiz-smart-bulb-to-home-assistant/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice integration of the Philips Smart LED, Look cool!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: Palno &amp; Slemon #2.2 Configuring and Validating</title><link>https://community.element14.com/thread/238782?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 11:40:07 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:6c65f9a8-13b3-4377-a58b-cb01e06ae9d2</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238782?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57324/palno-slemon-2-2-configuring-and-validating/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Good to hear that!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>Palno &amp; Slemon #2.2 Configuring and Validating</title><link>https://community.element14.com/thread/57324?ContentTypeID=0</link><pubDate>Sat, 03 Oct 2026 09:11:17 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:087eb976-b34a-427c-8bbf-dda3c2375293</guid><dc:creator>Qbit</dc:creator><slash:comments>6</slash:comments><comments>https://community.element14.com/thread/57324?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57324/palno-slemon-2-2-configuring-and-validating/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Hey this is part 2.2 for my projects Palno and Slemon together they both combine make up an accurate sleep tracker and with the power of UNO Q lets you wake up at the right time so you don`t feel tired and exhausted anymore,&amp;nbsp;&lt;/p&gt;
&lt;p&gt;you can read the past posts from here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57301/palno-slemon-1-introduction" data-e14adj="t"&gt;#1 Introduction&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57315/palno-slemon-2-1-configuring-and-validating" data-e14adj="t"&gt; #2.1 Configuring and Validating&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;In this I will be&amp;nbsp;configuring Arduino UNO Q and&amp;nbsp; HLK-LD2410B&amp;nbsp; &amp;nbsp;the mm wave sensor.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:tahoma, arial, helvetica, sans-serif;font-size:150%;"&gt;&lt;strong&gt;1) Arduino UNO Q&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;It comes pre-installed with debian image you don`t have to do anything at first but later you can update if you want and no cmd is required simply a message will pop-up asking you to update to the latest version, I think there is some sort of bug, sometimes when I had already installed the latest version the&amp;nbsp;&amp;nbsp;&lt;em&gt;install the latest version message pop up&lt;/em&gt; but it when I click to continue to just take away time and gives that the latest version is already installed.&amp;nbsp; (&lt;strong&gt;If you have updated it make sure if you are also using the arduino IDE for the sketch that you have installed the latest version of the board also if you don`t , somethings will not work as expected.&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;Thank you again to Element14 for awarding me the Arduino UNO Q and selecting me as the winner of the Top Tech voices.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/1000490558.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;As you power it up it will show nice boot animation on the led matrix then heart.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;You can use it as the standalone computer with a&amp;nbsp; USB-C dongle or&amp;nbsp; connect via usb(c) to your computer and use it.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;If you are using it with computer as I am, Download the App Labs&amp;nbsp;&lt;a href="https://docs.arduino.cc/software/app-lab/" rel="noopener noreferrer nofollow" target="_blank" data-e14adj="t"&gt;here.&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;First you connect it to the computer then it will show&amp;nbsp; in the app labs (&lt;em&gt;be patient it could take up to 40s for the Q to boot up.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/Screenshot-2026_2D00_10_2D00_01-163639.png" /&gt;&lt;/em&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Then click on it.&lt;/p&gt;
&lt;p&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/Screenshot-2026_2D00_10_2D00_01-163656.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;give it a name then&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/Screenshot-2026_2D00_10_2D00_01-163717.png" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;Setup wifi connection so you flash it&amp;nbsp; remotely.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/Screenshot-2026_2D00_10_2D00_01-163830.png" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;Setup a password that will be used when you are working with the terminal interface if you forget it you can change it in setting.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;also then it will ask for&amp;nbsp; a restart , after you do it then it will again ask to name it but that doesn`t work.(&lt;span style="font-size:75%;"&gt;see below),&amp;nbsp;&lt;span style="font-size:inherit;"&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;span style="font-size:75%;"&gt;[View:~/cfs-file/__key/communityserver-components-multipleuploadfilemanager/361ef60a_2D00_4b0f_2D00_4a8b_2D00_8c26_2D00_6c514eda7327-521898-complete/Screen-Recording-2026_2D00_10_2D00_01-171517.mp4:480:360]&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;So unlpug the board then replug it again then it will come to the home page.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:tahoma, arial, helvetica, sans-serif;font-size:150%;"&gt;2) HLK LD2410B mmWave Sensor:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;The radar module we are using for the motion tracking while sleep.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/HLK_2D00_LD2410_2D00_24Ghz_2D00_Human_2D00_Presence_2D00_Body_2D00_Induction_2D00_Distance_2D00_Detection_2D00_Radar_2D00_Sensor_2D00_Module_2D00_support_2D00_GPIO_2D00_and_2D00_UART_2D00_ports.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;Its pinout is this:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/ld_5F00_2D00_5F00_pinout.png" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;span style="font-family:inherit;"&gt;Thanks to shabaz!&lt;/span&gt;&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;span style="font-family:inherit;"&gt; I will not be using the OUT pin because it doesn`t give that much information only occupied/not occupied.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;span style="font-family:inherit;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;This was the most challenging firstly I was not able to find anything usable other then the post from&amp;nbsp;[mention:b0bc65b9ecdc4307bd967592f00e340a:e9ed411860ed4f2ba0265705b8793d05]&amp;nbsp;did some help you can see&amp;nbsp;&lt;a href="https://community.element14.com/technologies/embedded/b/blog/posts/experimenting-with-microwave-based-sensors-for-presence-detection" data-e14adj="t"&gt;here.&lt;/a&gt;&amp;nbsp;But I wanted more data then that so I scrolled a lot through datasheets and finally get some some useful datasheets not completely the source code but enough information about how does it function and what does it give in uart.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;This is the code I developed, I had also used AI because it could take me weeks to build.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:b9f5aba1-8192-45c0-9299-69280c7ba0bf:type=c_cpp&amp;text=%2F%0A%20%2A%20%20%201%20%20State%20%20%20%20%20%20%20%20%280%3Dnone%201%3Dmoving%202%3Dstatic%203%3Dboth%29%0A%20%2A%20%20%202%20%20MovDist_cm%0A%20%2A%20%20%203%20%20MovEnergy%0A%20%2A%20%20%204%20%20StaDist_cm%0A%20%2A%20%20%205%20%20DetDist_cm%0A%20%2A%20%20%206%20%20MG0%20%20%28gate%200%20%3D%200.00%E2%80%930.75%20m%29%0A%20%2A%20%20%207%20%20MG1%20%20%28gate%201%20%3D%200.75%E2%80%931.50%20m%29%0A%20%2A%20%20%208%20%20MG2%0A%20%2A%20%20%209%20%20MG3%0A%20%2A%20%20%2010%20MG4%0A%20%2A%20%20%2011%20MG5%0A%20%2A%20%20%2012%20MG6%0A%20%2A%20%20%2013%20MG7%0A%20%2A%20%20%2014%20MG8%0A%20%2A%20%20%2015%20SG3%20%20%28static%20gate%203%20onwards%20%E2%80%94%20SG0%2F1%2F2%20removed%29%0A%20%2A%20%20%2016%20SG4%0A%20%2A%20%20%2017%20SG5%0A%20%2A%20%20%2018%20SG6%0A%20%2A%20%20%2019%20SG7%0A%20%2A%20%20%2020%20SG8%0A%20%2A%20%20%2021%20Light%20%20%28photosensitive%200%E2%80%93255%29%0A%20%2A%20%20%2022%20OutPin%20%280%3Dempty%201%3Doccupied%29%0A%20%2A%0A%0A%20%2A%2F%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Config%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%23define%20LD2410_BAUD%20%20%20%20%20%20256000%20%20%20%2F%2F%20Change%20to%20115200%20for%20LD2410S%2FD%0A%23define%20DBG_BAUD%20%20%20%20%20%20%20%20%20115200%0A%23define%20RX_PIN%20%20%20%20%20%20%20%20%20%20%2016%0A%23define%20TX_PIN%20%20%20%20%20%20%20%20%20%20%2017%0A%23define%20PLOT_MS%20%20%20%20%20%20%20%20%20%2050%20%20%20%20%20%20%20%2F%2F%2020%20Hz%20output%20%E2%80%94%20fast%20like%20Arduino%20plotter%0A%23define%20MAX_GATES%20%20%20%20%20%20%20%209%0A%23define%20FRAME_BUF_SIZE%20%20%20128%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Protocol%20bytes%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%23define%20DH0%200xF4%0A%23define%20DH1%200xF3%0A%23define%20DH2%200xF2%0A%23define%20DH3%200xF1%0A%23define%20DF0%200xF8%0A%23define%20DF1%200xF7%0A%23define%20DF2%200xF6%0A%23define%20DF3%200xF5%0A%0A%23define%20CH0%200xFD%0A%23define%20CH1%200xFC%0A%23define%20CH2%200xFB%0A%23define%20CH3%200xFA%0A%23define%20CF0%200x04%0A%23define%20CF1%200x03%0A%23define%20CF2%200x02%0A%23define%20CF3%200x01%0A%0A%23define%20CMD_EN_CFG%20%20%200x00FF%0A%23define%20CMD_END_CFG%20%200x00FE%0A%23define%20CMD_EN_ENG%20%20%200x0062%0A%23define%20CMD_CL_ENG%20%20%200x0063%0A%23define%20CMD_RD_VER%20%20%200x00A0%0A%23define%20CMD_RD_PRM%20%20%200x0061%0A%23define%20CMD_SET_DIST%200x0060%0A%23define%20CMD_SET_SENS%200x0064%0A%23define%20CMD_RESET%20%20%20%200x00A2%0A%23define%20CMD_RESTART%20%200x00A3%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Global%20variables%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0Astatic%20uint8_t%20%20fb%5BFRAME_BUF_SIZE%5D%3B%0Astatic%20uint16_t%20fbi%20%20%3D%200%3B%0Astatic%20uint16_t%20fexp%20%3D%200%3B%0Astatic%20uint16_t%20fcnt%20%3D%200%3B%0Astatic%20uint8_t%20%20hi%20%20%20%3D%200%3B%0Astatic%20uint8_t%20%20hb%5B4%5D%3B%0Astatic%20bool%20%20%20%20%20isDataFrame%20%3D%20false%3B%0A%0Aenum%20%7B%20ST_HDR%2C%20ST_LEN%2C%20ST_DATA%2C%20ST_FOOT%20%7D%20rxSt%20%3D%20ST_HDR%3B%0A%0Astatic%20bool%20%20%20%20%20newData%20%20%20%20%3D%20false%3B%0Astatic%20bool%20%20%20%20%20streaming%20%20%3D%20false%3B%20%20%2F%2F%20%E2%86%90%20starts%20false%3B%20%27n%27%20enables%0Astatic%20bool%20%20%20%20%20csvMode%20%20%20%20%3D%20false%3B%0A%0A%2F%2F%20Sensor%20readings%20%E2%80%94%20raw%2C%20no%20ceiling%20applied%0Astatic%20uint8_t%20%20g_state%20%20%20%3D%200%3B%0Astatic%20uint16_t%20g_movDist%20%3D%200%3B%20%20%20%2F%2F%20cm%0Astatic%20uint16_t%20g_movEn%20%20%20%3D%200%3B%20%20%20%2F%2F%20energy%20%28sensor%20outputs%200-100%29%0Astatic%20uint16_t%20g_staDist%20%3D%200%3B%20%20%20%2F%2F%20cm%0Astatic%20uint16_t%20g_detDist%20%3D%200%3B%20%20%20%2F%2F%20cm%0Astatic%20uint8_t%20%20g_maxMov%20%20%3D%208%3B%0Astatic%20uint8_t%20%20g_maxSta%20%20%3D%208%3B%0Astatic%20uint16_t%20g_movG%5BMAX_GATES%5D%3B%20%20%2F%2F%20per-gate%20motion%20energy%0Astatic%20uint16_t%20g_staG%5BMAX_GATES%5D%3B%20%20%2F%2F%20per-gate%20static%20energy%0Astatic%20uint16_t%20g_light%20%20%20%3D%200%3B%0Astatic%20uint8_t%20%20g_out%20%20%20%20%20%3D%200%3B%0A%0Astatic%20unsigned%20long%20lastPlot%20%3D%200%3B%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Command%20helpers%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Astatic%20void%20sendCmd%28uint16_t%20word%2C%20const%20uint8_t%2A%20dat%2C%20uint8_t%20dlen%29%20%7B%0A%20%20uint16_t%20ilen%20%3D%202%20%2B%20dlen%3B%0A%20%20uint8_t%20buf%5B32%5D%3B%0A%20%20uint8_t%20i%20%3D%200%3B%0A%20%20buf%5Bi%2B%2B%5D%3DCH0%3B%20buf%5Bi%2B%2B%5D%3DCH1%3B%20buf%5Bi%2B%2B%5D%3DCH2%3B%20buf%5Bi%2B%2B%5D%3DCH3%3B%0A%20%20buf%5Bi%2B%2B%5D%3Dilen%260xFF%3B%20buf%5Bi%2B%2B%5D%3Dilen%3E%3E8%3B%0A%20%20buf%5Bi%2B%2B%5D%3Dword%260xFF%3B%20buf%5Bi%2B%2B%5D%3Dword%3E%3E8%3B%0A%20%20for%20%28uint8_t%20j%3D0%3B%20j%3Cdlen%20%26%26%20j%3C20%3B%20j%2B%2B%29%20buf%5Bi%2B%2B%5D%3Ddat%5Bj%5D%3B%0A%20%20buf%5Bi%2B%2B%5D%3DCF0%3B%20buf%5Bi%2B%2B%5D%3DCF1%3B%20buf%5Bi%2B%2B%5D%3DCF2%3B%20buf%5Bi%2B%2B%5D%3DCF3%3B%0A%20%20Serial2.write%28buf%2C%20i%29%3B%0A%20%20Serial2.flush%28%29%3B%0A%7D%0A%0Astatic%20void%20enCfg%28%29%20%20%7B%20uint8_t%20v%5B%5D%3D%7B0x01%2C0x00%7D%3B%20sendCmd%28CMD_EN_CFG%2C%20v%2C%202%29%3B%20delay%2880%29%3B%20%7D%0Astatic%20void%20endCfg%28%29%20%7B%20sendCmd%28CMD_END_CFG%2C%20NULL%2C%200%29%3B%20delay%2880%29%3B%20%7D%0A%0Astatic%20void%20cmdEnEng%28%29%20%20%7B%20enCfg%28%29%3B%20sendCmd%28CMD_EN_ENG%2C%20NULL%2C0%29%3B%20delay%2880%29%3B%20endCfg%28%29%3B%20%7D%0Astatic%20void%20cmdVer%28%29%20%20%20%20%7B%20enCfg%28%29%3B%20sendCmd%28CMD_RD_VER%2C%20NULL%2C0%29%3B%20delay%28150%29%3B%20endCfg%28%29%3B%20%7D%0Astatic%20void%20cmdParam%28%29%20%20%7B%20enCfg%28%29%3B%20sendCmd%28CMD_RD_PRM%2C%20NULL%2C0%29%3B%20delay%28150%29%3B%20endCfg%28%29%3B%20%7D%0Astatic%20void%20cmdReset%28%29%20%20%7B%20enCfg%28%29%3B%20sendCmd%28CMD_RESET%2C%20%20NULL%2C0%29%3B%20delay%28150%29%3B%20endCfg%28%29%3B%20%7D%0Astatic%20void%20cmdRestart%28%29%7B%20enCfg%28%29%3B%20sendCmd%28CMD_RESTART%2CNULL%2C0%29%3B%20delay%28150%29%3B%20%7D%0A%0Astatic%20void%20cmdSetDist%28uint8_t%20mg%2C%20uint8_t%20sg%2C%20uint16_t%20t%29%20%7B%0A%20%20uint8_t%20v%5B18%5D%3D%7B%0A%20%20%20%200x00%2C0x00%2Cmg%2C0%2C0%2C0%2C%0A%20%20%20%200x01%2C0x00%2Csg%2C0%2C0%2C0%2C%0A%20%20%20%200x02%2C0x00%2C%28uint8_t%29%28t%260xFF%29%2C%28uint8_t%29%28t%3E%3E8%29%2C0%2C0%0A%20%20%7D%3B%0A%20%20enCfg%28%29%3B%20sendCmd%28CMD_SET_DIST%2C%20v%2C%2018%29%3B%20delay%2880%29%3B%20endCfg%28%29%3B%0A%7D%0A%0Astatic%20void%20cmdSetSens%28uint8_t%20m%2C%20uint8_t%20s%29%20%7B%0A%20%20uint8_t%20v%5B18%5D%3D%7B%0A%20%20%20%200x00%2C0x00%2C0xFF%2C0xFF%2C0%2C0%2C%0A%20%20%20%200x01%2C0x00%2Cm%2C0%2C0%2C0%2C%0A%20%20%20%200x02%2C0x00%2Cs%2C0%2C0%2C0%0A%20%20%7D%3B%0A%20%20enCfg%28%29%3B%20sendCmd%28CMD_SET_SENS%2C%20v%2C%2018%29%3B%20delay%2880%29%3B%20endCfg%28%29%3B%0A%7D%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Frame%20parser%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Astatic%20bool%20match4%28const%20uint8_t%2A%20a%2C%20uint8_t%20b0%2Cuint8_t%20b1%2Cuint8_t%20b2%2Cuint8_t%20b3%29%7B%0A%20%20return%20a%5B0%5D%3D%3Db0%20%26%26%20a%5B1%5D%3D%3Db1%20%26%26%20a%5B2%5D%3D%3Db2%20%26%26%20a%5B3%5D%3D%3Db3%3B%0A%7D%0A%0Astatic%20void%20parseData%28%29%20%7B%0A%20%20uint16_t%20ilen%20%3D%20fb%5B4%5D%20%7C%20%28fb%5B5%5D%3C%3C8%29%3B%0A%20%20if%20%28ilen%20%3C%2012%29%20return%3B%0A%20%20uint16_t%20fp%20%3D%206%20%2B%20ilen%3B%0A%20%20if%20%28fp%2B4%20%3E%20fbi%29%20return%3B%0A%20%20if%20%28%21match4%28fb%2Bfp%2C%20DF0%2CDF1%2CDF2%2CDF3%29%29%20return%3B%0A%0A%20%20const%20uint8_t%2A%20d%20%3D%20fb%2B6%3B%0A%20%20uint8_t%20dtype%20%3D%20d%5B0%5D%3B%0A%20%20if%20%28d%5B1%5D%20%21%3D%200xAA%29%20return%3B%0A%0A%20%20g_state%20%20%20%3D%20d%5B2%5D%3B%0A%20%20g_movDist%20%3D%20%28uint16_t%29d%5B3%5D%7C%28%28uint16_t%29d%5B4%5D%3C%3C8%29%3B%0A%20%20g_movEn%20%20%20%3D%20d%5B5%5D%3B%0A%20%20g_staDist%20%3D%20%28uint16_t%29d%5B6%5D%7C%28%28uint16_t%29d%5B7%5D%3C%3C8%29%3B%0A%20%20%2F%2F%20d%5B8%5D%20%3D%20staEnergy%20%E2%80%94%20intentionally%20NOT%20stored%20%28column%205%20removed%29%0A%20%20g_detDist%20%3D%20%28uint16_t%29d%5B9%5D%7C%28%28uint16_t%29d%5B10%5D%3C%3C8%29%3B%0A%0A%20%20if%20%28dtype%20%3D%3D%200x01%20%26%26%20ilen%20%3E%3D%2020%29%20%7B%0A%20%20%20%20g_maxMov%20%3D%20d%5B11%5D%3B%0A%20%20%20%20g_maxSta%20%3D%20d%5B12%5D%3B%0A%20%20%20%20if%20%28g_maxMov%20%3E%3D%20MAX_GATES%29%20g_maxMov%20%3D%20MAX_GATES-1%3B%0A%20%20%20%20if%20%28g_maxSta%20%3E%3D%20MAX_GATES%29%20g_maxSta%20%3D%20MAX_GATES-1%3B%0A%0A%20%20%20%20uint8_t%20idx%20%3D%2013%3B%0A%20%20%20%20for%20%28uint8_t%20i%3D0%3B%20i%3C%3Dg_maxMov%3B%20i%2B%2B%29%0A%20%20%20%20%20%20g_movG%5Bi%5D%20%3D%20%28idx%20%3C%20ilen%2B6%29%20%3F%20d%5Bidx%2B%2B%5D%20%3A%200%3B%0A%20%20%20%20for%20%28uint8_t%20i%3Dg_maxMov%2B1%3B%20i%3CMAX_GATES%3B%20i%2B%2B%29%20g_movG%5Bi%5D%3D0%3B%0A%0A%20%20%20%20for%20%28uint8_t%20i%3D0%3B%20i%3C%3Dg_maxSta%3B%20i%2B%2B%29%0A%20%20%20%20%20%20g_staG%5Bi%5D%20%3D%20%28idx%20%3C%20ilen%2B6%29%20%3F%20d%5Bidx%2B%2B%5D%20%3A%200%3B%0A%20%20%20%20for%20%28uint8_t%20i%3Dg_maxSta%2B1%3B%20i%3CMAX_GATES%3B%20i%2B%2B%29%20g_staG%5Bi%5D%3D0%3B%0A%0A%20%20%20%20g_light%20%3D%20%28idx%20%3C%20ilen%2B6%29%20%3F%20d%5Bidx%2B%2B%5D%20%3A%200%3B%0A%20%20%20%20g_out%20%20%20%3D%20%28idx%20%3C%20ilen%2B6%29%20%3F%20d%5Bidx%5D%20%20%20%3A%200%3B%0A%20%20%7D%0A%0A%20%20newData%20%3D%20true%3B%0A%7D%0A%0Astatic%20void%20parseAck%28%29%20%7B%0A%20%20uint16_t%20ilen%20%3D%20fb%5B4%5D%7C%28fb%5B5%5D%3C%3C8%29%3B%0A%20%20if%20%28ilen%20%3C%204%29%20return%3B%0A%20%20uint16_t%20fp%20%3D%206%2Bilen%3B%0A%20%20if%20%28fp%2B4%20%3E%20fbi%20%7C%7C%20%21match4%28fb%2Bfp%2CCF0%2CCF1%2CCF2%2CCF3%29%29%20return%3B%0A%20%20uint16_t%20ack%20%3D%20fb%5B6%5D%7C%28fb%5B7%5D%3C%3C8%29%3B%0A%20%20uint16_t%20st%20%20%3D%20fb%5B8%5D%7C%28fb%5B9%5D%3C%3C8%29%3B%0A%20%20if%20%28ack%3D%3D0x01A0%20%26%26%20st%3D%3D0%20%26%26%20ilen%3E%3D10%29%0A%20%20%20%20Serial.printf%28%22%23%20FW%20type%3D%25d%20V%25d.%2502X%5Cn%22%2Cfb%5B10%5D%2Cfb%5B11%5D%2Cfb%5B14%5D%29%3B%0A%20%20if%20%28ack%3D%3D0x0161%20%26%26%20st%3D%3D0%20%26%26%20ilen%3E%3D30%29%20%7B%0A%20%20%20%20Serial.printf%28%22%23%20Params%3A%20maxMov%3D%25d%20maxSta%3D%25d%20timeout%3D%25ds%5Cn%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fb%5B11%5D%2Cfb%5B12%5D%2C%20fb%5B31%5D%7C%28fb%5B32%5D%3C%3C8%29%29%3B%0A%20%20%7D%0A%7D%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20UART%20state%20machine%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Astatic%20void%20readRadar%28%29%20%7B%0A%20%20while%20%28Serial2.available%28%29%29%20%7B%0A%20%20%20%20uint8_t%20b%20%3D%20Serial2.read%28%29%3B%0A%20%20%20%20switch%20%28rxSt%29%20%7B%0A%20%20%20%20%20%20case%20ST_HDR%3A%0A%20%20%20%20%20%20%20%20hb%5Bhi%2B%2B%5D%3Db%3B%0A%20%20%20%20%20%20%20%20if%20%28hi%3C4%29%20break%3B%0A%20%20%20%20%20%20%20%20if%20%20%20%20%20%20%28match4%28hb%2CDH0%2CDH1%2CDH2%2CDH3%29%29%7BisDataFrame%3Dtrue%3B%20memcpy%28fb%2Chb%2C4%29%3Bfbi%3D4%3Bfcnt%3D0%3BrxSt%3DST_LEN%3B%7D%0A%20%20%20%20%20%20%20%20else%20if%20%28match4%28hb%2CCH0%2CCH1%2CCH2%2CCH3%29%29%7BisDataFrame%3Dfalse%3Bmemcpy%28fb%2Chb%2C4%29%3Bfbi%3D4%3Bfcnt%3D0%3BrxSt%3DST_LEN%3B%7D%0A%20%20%20%20%20%20%20%20else%20%7B%20hb%5B0%5D%3Dhb%5B1%5D%3Bhb%5B1%5D%3Dhb%5B2%5D%3Bhb%5B2%5D%3Dhb%5B3%5D%3Bhi%3D3%3B%20%7D%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20case%20ST_LEN%3A%0A%20%20%20%20%20%20%20%20if%20%28fbi%3CFRAME_BUF_SIZE%29%20fb%5Bfbi%2B%2B%5D%3Db%3B%20fcnt%2B%2B%3B%0A%20%20%20%20%20%20%20%20if%20%28fcnt%3E%3D2%29%20%7B%0A%20%20%20%20%20%20%20%20%20%20fexp%3Dfb%5B4%5D%7C%28%28uint16_t%29fb%5B5%5D%3C%3C8%29%3B%0A%20%20%20%20%20%20%20%20%20%20if%20%28fexp%3D%3D0%7C%7Cfexp%3EFRAME_BUF_SIZE-12%29%7BrxSt%3DST_HDR%3Bhi%3D0%3Bfbi%3D0%3Bfcnt%3D0%3B%7D%0A%20%20%20%20%20%20%20%20%20%20else%20%7Bfcnt%3D0%3BrxSt%3DST_DATA%3B%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20case%20ST_DATA%3A%0A%20%20%20%20%20%20%20%20if%20%28fbi%3CFRAME_BUF_SIZE%29%20fb%5Bfbi%2B%2B%5D%3Db%3B%20fcnt%2B%2B%3B%0A%20%20%20%20%20%20%20%20if%20%28fcnt%3E%3Dfexp%29%7Bfcnt%3D0%3BrxSt%3DST_FOOT%3B%7D%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20case%20ST_FOOT%3A%0A%20%20%20%20%20%20%20%20if%20%28fbi%3CFRAME_BUF_SIZE%29%20fb%5Bfbi%2B%2B%5D%3Db%3B%20fcnt%2B%2B%3B%0A%20%20%20%20%20%20%20%20if%20%28fcnt%3E%3D4%29%7B%0A%20%20%20%20%20%20%20%20%20%20if%20%28isDataFrame%29%20parseData%28%29%3B%20else%20parseAck%28%29%3B%0A%20%20%20%20%20%20%20%20%20%20rxSt%3DST_HDR%3Bhi%3D0%3Bfbi%3D0%3Bfcnt%3D0%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Output%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Astatic%20void%20printHeader%28%29%20%7B%0A%20%20char%20sep%20%3D%20csvMode%20%3F%20%27%2C%27%20%3A%20%27%5Ct%27%3B%0A%20%20%2F%2F%20Engineering%20mode%20columns%20%2822%20total%20%E2%80%94%20cols%205%2F16%2F17%2F18%20removed%29%0A%20%20Serial.print%28F%28%22State%22%29%29%3B%20%20%20%20%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28F%28%22MovDist_cm%22%29%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28F%28%22MovEnergy%22%29%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28F%28%22StaDist_cm%22%29%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28F%28%22DetDist_cm%22%29%29%3B%20Serial.print%28sep%29%3B%0A%20%20%2F%2F%20Moving%20gate%20energies%20MG0%E2%80%93MG8%0A%20%20for%20%28uint8_t%20i%3D0%3Bi%3CMAX_GATES%3Bi%2B%2B%29%7B%0A%20%20%20%20Serial.print%28F%28%22MG%22%29%29%3B%20Serial.print%28i%29%3B%20Serial.print%28sep%29%3B%0A%20%20%7D%0A%20%20%2F%2F%20Static%20gate%20energies%20SG3%E2%80%93SG8%20only%20%28SG0%2CSG1%2CSG2%20removed%29%0A%20%20for%20%28uint8_t%20i%3D3%3Bi%3CMAX_GATES%3Bi%2B%2B%29%7B%0A%20%20%20%20Serial.print%28F%28%22SG%22%29%29%3B%20Serial.print%28i%29%3B%20Serial.print%28sep%29%3B%0A%20%20%7D%0A%20%20Serial.print%28F%28%22Light%22%29%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.println%28F%28%22OutPin%22%29%29%3B%0A%7D%0A%0Astatic%20void%20outputLine%28%29%20%7B%0A%20%20char%20sep%20%3D%20csvMode%20%3F%20%27%2C%27%20%3A%20%27%5Ct%27%3B%0A%0A%20%20Serial.print%28g_state%29%3B%20%20%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28g_movDist%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28g_movEn%29%3B%20%20%20Serial.print%28sep%29%3B%0A%20%20Serial.print%28g_staDist%29%3B%20Serial.print%28sep%29%3B%0A%20%20%2F%2F%20col%205%20%28StaEnergy%29%20skipped%0A%20%20Serial.print%28g_detDist%29%3B%20Serial.print%28sep%29%3B%0A%0A%20%20%2F%2F%20MG0%E2%80%93MG8%0A%20%20for%20%28uint8_t%20i%3D0%3Bi%3CMAX_GATES%3Bi%2B%2B%29%7B%0A%20%20%20%20Serial.print%28g_movG%5Bi%5D%29%3B%20Serial.print%28sep%29%3B%0A%20%20%7D%0A%20%20%2F%2F%20SG3%E2%80%93SG8%20only%20%28SG0%2CSG1%2CSG2%20skipped%29%0A%20%20for%20%28uint8_t%20i%3D3%3Bi%3CMAX_GATES%3Bi%2B%2B%29%7B%0A%20%20%20%20Serial.print%28g_staG%5Bi%5D%29%3B%20Serial.print%28sep%29%3B%0A%20%20%7D%0A%20%20Serial.print%28g_light%29%3B%20Serial.print%28sep%29%3B%0A%20%20Serial.println%28g_out%29%3B%0A%7D%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Command%20handler%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Astatic%20void%20printHelp%28%29%20%7B%0A%20%20Serial.println%28F%28%22%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20LD2410%20Plotter%20Commands%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20STREAM%3A%20n%3DStart%20streaming%20%20x%3DStop%20streaming%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20FMT%3A%20%20%20%20t%3DTab%28Plotter%2FPython%29%20%20c%3DCSV%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20INFO%3A%20%20%20H%3DHeader%20%20v%3DFirmware%20%20p%3DParams%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20SENS%3A%20%20%201%3D5%20%202%3D15%20%203%3D30%20%204%3D50%28def%29%20%205%3D75%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20CFG%3A%20%20%20%20d%3DMaxDist8%2F5s%20%20r%3DFactoryReset%20%20R%3DRestart%22%29%29%3B%0A%20%20Serial.println%28F%28%22%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%22%29%29%3B%0A%7D%0A%0Astatic%20void%20handleCmd%28%29%20%7B%0A%20%20if%20%28%21Serial.available%28%29%29%20return%3B%0A%20%20char%20c%20%3D%20Serial.read%28%29%3B%0A%20%20while%20%28Serial.available%28%29%29%20Serial.read%28%29%3B%0A%0A%20%20switch%20%28c%29%20%7B%0A%20%20%20%20case%20%27n%27%3A%0A%20%20%20%20%20%20%2F%2F%20Start%20streaming%3A%20enable%20engineering%20mode%20then%20begin%20output%0A%20%20%20%20%20%20if%20%28%21streaming%29%20%7B%0A%20%20%20%20%20%20%20%20Serial.println%28F%28%22%23%20Enabling%20engineering%20mode...%22%29%29%3B%0A%20%20%20%20%20%20%20%20cmdEnEng%28%29%3B%0A%20%20%20%20%20%20%20%20delay%28200%29%3B%0A%20%20%20%20%20%20%20%20readRadar%28%29%3B%20%2F%2F%20drain%20ACKs%0A%20%20%20%20%20%20%20%20streaming%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20printHeader%28%29%3B%0A%20%20%20%20%20%20%20%20Serial.println%28F%28%22%23%20STREAMING%20START%22%29%29%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20break%3B%0A%0A%20%20%20%20case%20%27x%27%3A%0A%20%20%20%20%20%20streaming%20%3D%20false%3B%0A%20%20%20%20%20%20Serial.println%28F%28%22%23%20STREAMING%20STOP%20%E2%80%94%20send%20%27n%27%20to%20resume%22%29%29%3B%0A%20%20%20%20%20%20break%3B%0A%0A%20%20%20%20case%20%27t%27%3A%20csvMode%3Dfalse%3B%20%20%20%20%20Serial.println%28F%28%22%23%20Tab%20mode%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27c%27%3A%20csvMode%3Dtrue%3B%20%20%20%20%20%20Serial.println%28F%28%22%23%20CSV%20mode%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27H%27%3A%20printHeader%28%29%3B%20break%3B%0A%20%20%20%20case%20%27v%27%3A%20cmdVer%28%29%3B%20%20%20Serial.println%28F%28%22%23%20FW%20query%20sent%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27p%27%3A%20cmdParam%28%29%3B%20Serial.println%28F%28%22%23%20Params%20query%20sent%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%271%27%3A%20cmdSetSens%28%205%2C%205%29%3B%20Serial.println%28F%28%22%23%20Sens%3D5%2F5%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%272%27%3A%20cmdSetSens%2815%2C15%29%3B%20Serial.println%28F%28%22%23%20Sens%3D15%2F15%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%273%27%3A%20cmdSetSens%2830%2C30%29%3B%20Serial.println%28F%28%22%23%20Sens%3D30%2F30%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%274%27%3A%20cmdSetSens%2850%2C50%29%3B%20Serial.println%28F%28%22%23%20Sens%3D50%2F50%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%275%27%3A%20cmdSetSens%2875%2C75%29%3B%20Serial.println%28F%28%22%23%20Sens%3D75%2F75%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27d%27%3A%20cmdSetDist%288%2C8%2C5%29%3B%20Serial.println%28F%28%22%23%20MaxDist%3D8%2F8%205s%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27r%27%3A%20cmdReset%28%29%3B%20%20%20Serial.println%28F%28%22%23%20Factory%20reset%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27R%27%3A%20cmdRestart%28%29%3B%20Serial.println%28F%28%22%23%20Restart%20sent%22%29%29%3B%20break%3B%0A%20%20%20%20case%20%27h%27%3A%20case%20%27%3F%27%3A%20printHelp%28%29%3B%20break%3B%0A%20%20%20%20default%3A%20%20Serial.printf%28%22%23%20Unknown%20%27%25c%27%20%E2%80%94%20h%3Dhelp%5Cn%22%2Cc%29%3B%20break%3B%0A%20%20%7D%0A%7D%0A%0A%2F%2F%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Setup%20%26%20Loop%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0Avoid%20setup%28%29%20%7B%0A%20%20Serial.begin%28DBG_BAUD%29%3B%0A%20%20while%20%28%21Serial%29%20delay%285%29%3B%0A%0A%20%20memset%28g_movG%2C%200%2C%20sizeof%28g_movG%29%29%3B%0A%20%20memset%28g_staG%2C%200%2C%20sizeof%28g_staG%29%29%3B%0A%0A%20%20Serial.println%28F%28%22%23%20LD2410%20Raw%20Plotter%20booting...%22%29%29%3B%0A%20%20Serial.printf%28%22%23%20Radar%20baud%3D%25d%20%7C%20Plot%20interval%3D%25dms%5Cn%22%2C%20LD2410_BAUD%2C%20PLOT_MS%29%3B%0A%0A%20%20Serial2.begin%28LD2410_BAUD%2C%20SERIAL_8N1%2C%20RX_PIN%2C%20TX_PIN%29%3B%0A%20%20delay%281500%29%3B%0A%0A%20%20cmdVer%28%29%3B%20%20%20delay%28200%29%3B%0A%20%20cmdParam%28%29%3B%20delay%28200%29%3B%0A%20%20readRadar%28%29%3B%20%2F%2F%20drain%20ACK%20responses%0A%0A%20%20Serial.println%28F%28%22%23%20Ready.%20Send%20%27n%27%20to%20start%20streaming%2C%20%27h%27%20for%20help.%22%29%29%3B%0A%7D%0A%0Avoid%20loop%28%29%20%7B%0A%20%20%2F%2F%20Always%20read%20radar%20data%20from%20UART%20%28keeps%20buffer%20clear%20even%20when%20not%20streaming%29%0A%20%20readRadar%28%29%3B%0A%0A%20%20%2F%2F%20Handle%20user%2FPython%20commands%0A%20%20handleCmd%28%29%3B%0A%0A%20%20%2F%2F%20Only%20output%20data%20when%20streaming%20is%20active%0A%20%20if%20%28streaming%20%26%26%20newData%20%26%26%20%28millis%28%29-lastPlot%20%3E%3D%20PLOT_MS%29%29%20%7B%0A%20%20%20%20lastPlot%20%3D%20millis%28%29%3B%0A%20%20%20%20newData%20%20%3D%20false%3B%0A%20%20%20%20outputLine%28%29%3B%0A%20%20%7D%0A%7D]&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="2"&gt;To extract reliable telemetry without dropping bytes, the firmware processes incoming UART data using a non-blocking finite state machine (&lt;code data-path-to-node="2" data-index-in-node="139"&gt;ST_HDR,&lt;/code&gt;&amp;nbsp;&lt;code data-path-to-node="2" data-index-in-node="158"&gt;ST_LEN&lt;/code&gt; &amp;nbsp;&lt;code data-path-to-node="2" data-index-in-node="177"&gt;ST_DATA&lt;/code&gt; &amp;nbsp;&lt;code data-path-to-node="2" data-index-in-node="197"&gt;ST_FOOT&lt;/code&gt;). It continuously inspects the serial stream for the radar&amp;#39;s signature 4-byte headers&amp;mdash;&lt;code data-path-to-node="2" data-index-in-node="291"&gt;0xF4 0xF3 0xF2 0xF1&lt;/code&gt; for active data frames and &lt;code data-path-to-node="2" data-index-in-node="338"&gt;0xFD 0xFC 0xFB 0xFA&lt;/code&gt; for command acknowledgments. Once a valid payload is buffered and verified against its frame footer, &lt;code data-path-to-node="2" data-index-in-node="459"&gt;parseData()&lt;/code&gt; unpacks state flags, target distances, total motion energy, and individual per-gate thresholds directly into global variables.&lt;/p&gt;
&lt;p data-path-to-node="3"&gt;For easier host synchronization, data streaming remains paused on boot until an &lt;code data-path-to-node="3" data-index-in-node="80"&gt;&amp;#39;n&amp;#39;&lt;/code&gt; command is sent over the Serial Monitor or through a companion Python script. Once triggered, the sketch streams 22 columns of tab-separated or CSV values at 20 Hz (every 50 ms). This output rate is fast enough for smooth real-time plotting while keeping serial overhead low enough to prevent lag during long sleep-monitoring sessions.&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;You can also adjust parameters interactively without reflashing the board. Simple single-character keyboard inputs let you tweak distance gate sensitivities (&lt;code data-path-to-node="4" data-index-in-node="158"&gt;1&lt;/code&gt; through &lt;code data-path-to-node="4" data-index-in-node="168"&gt;5&lt;/code&gt;), query active firmware parameters, or trigger hardware resets on the fly. This setup makes it simple to capture clean signal data for the Arduino Serial Plotter or stream it into external Python scripts for further sleep-stage analysis.&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;This the testing data I captured:&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/ld2410_5F00_recording_5F00_20261002_5F00_194308.mp4:640:360]&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;In the next part&amp;nbsp; I will collect data to train the models and understand it to make the suitable algorithm.&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;Thank you for reading !&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;span style="font-family:tahoma, arial, helvetica, sans-serif;font-size:150%;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-family:inherit;font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;</description></item><item><title>RE: Palno &amp; Slemon #2.2 Configuring and Validating</title><link>https://community.element14.com/thread/238781?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 11:39:40 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:075122bd-1d23-4e57-a01c-670489aef0db</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238781?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57324/palno-slemon-2-2-configuring-and-validating/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Sorry then I can`t do anything:(&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: Palno &amp; Slemon #3 Building the Algorithm</title><link>https://community.element14.com/thread/238779?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 09:12:03 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:76b50425-d78b-4dd3-9b3a-756bdd303b9a</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238779?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57366/palno-slemon-3-building-the-algorithm/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Thank you for you encouragement!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>Palno &amp; Slemon #3 Building the Algorithm</title><link>https://community.element14.com/thread/57366?ContentTypeID=0</link><pubDate>Thu, 22 Oct 2026 16:22:14 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:2997b5b1-61a8-49bb-8dca-90d351b61152</guid><dc:creator>Qbit</dc:creator><slash:comments>2</slash:comments><comments>https://community.element14.com/thread/57366?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57366/palno-slemon-3-building-the-algorithm/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Hi, I hope you are doing well, and I am really sorry for the delay&amp;nbsp; in posting this part, I was a little lost and busy with other work. But I will give my best finishing this project and making it reliable.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span style="font-size:150%;"&gt;A Quick Recap&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&amp;nbsp;I was working on project&amp;nbsp; that uses&amp;nbsp; data from multiple resources like breathing data from the radar module,&amp;nbsp; heart beat rate , blood oxygen level to detect&amp;nbsp; the stage of sleep you are in to help you wake up at the right time so you wouldn`t feel tired after you wake up, because wake you up at the right time.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;These are the past posts:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57301/palno-slemon-1-introduction" data-e14adj="t"&gt;Palno &amp;amp; Slemon #1 Introduction&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57315/palno-slemon-2-1-configuring-and-validating" data-e14adj="t"&gt;Palno &amp;amp; Slemon #2.1 Configuring and Validating&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57324/palno-slemon-2-2-configuring-and-validating" data-e14adj="t"&gt;Palno &amp;amp; Slemon #2.2 Configuring and Validating&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;Algorithm&lt;/strong&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;We need to go there in two places:
&lt;ol&gt;
&lt;li&gt;In the radar module&lt;/li&gt;
&lt;li&gt;In the heart rate and blood oxygen level sensor&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;First,&amp;nbsp; radar module. I have thought of using a model to detect breath but I wasn&amp;#39;t successful as&amp;nbsp; it gave me terrible results, as I will show you soon.&amp;nbsp;&lt;/p&gt;
We already have the algorithm from the resources provided by the analog devices but I would like to explain how it works.&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;The Radar (HLK-LD2410)&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/hlk.png" /&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;(Primarily this is the one I am using.)&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;I would also like to share the part of the journey of making it, though I had not used it, finally, because it was really terrible.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;Firstly I tried training the model but in the future I will see that it will become a terrible waste of time.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;I first decided to collect data for that. As I am using esp32 for test purposes, I use its web capabilities to collect data and control pause and resume via the web interface. Do it a little bit.Then I collect a little bit of data, at least 93 samples: 46 for breathing data and 47 for normal data, like the background, when we hold the breath and train. We save it as a CSV, which is also timestamped.I also used Python with PySerial to communicate with the ESP32 and capture the data. From the web interface when the button switched on, it saves the data of the given time and stamps it with a time tag in milliseconds, assigns all the values, and saves it as CSV.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Screenshot-2026_2D00_09_2D00_13-231641.png" /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;(This is the representation of a CSV dataset I made but it is not the same as what I used.)&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;There were many axes, like all the data the data module can give, so I don&amp;#39;t show the graph here in the web interface because it will crash. It could crash and it is ready to. I had to use AI for this because I don&amp;#39;t have a lot of time to finish it all up.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Screenshot-2026_2D00_10_2D00_18-193247.png" /&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;Screenshot of the web interface hosted on the ESP32&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;Sadly I was not able to use it because I was not able to capture a lot of sensible data. Though I have enough computing power on the Arduino Q for the project, the 100 samples (approximately) I collected are not enough to train that complex model, as I will show you later in the graph. This is because there were nearly a lot of delays and distortion during the collection of data. The sensor is good at its job collecting a lot of data and training a model with a lot of axes requires the equivalent amount of data.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Screenshot-2026_2D00_10_2D00_21-222156.png" /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;I know this completely looks terrible and I really have wasted 4 to 5 days doing this.&lt;br /&gt;&lt;br /&gt;Firstly I had got accuracy pretty well 50%, not that pretty well but better than 0%. I know, 50% was not that usable though. I thought after doing some more improvements I will really improve it but that wasn&amp;#39;t the case. I even distorted the 50% and fell into 0%. Absolutely terrible.&lt;br /&gt;&lt;br /&gt;This is how I lean towards the algorithm, with the stress that I had to it finish in 2 days.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:d8f92d58-af06-4c28-8700-72a047df945e:type=text&amp;text=%0D%0A3%0930%09100%0930%090%09100%09100%0934%0910%099%0910%092%0911%098%09100%09100%0980%0941%0941%0928%091%091%0D%0A3%0953%09100%0930%090%09100%09100%09100%0911%099%092%095%098%095%09100%09100%0980%0942%0942%0929%091%091%0D%0A3%0966%09100%0953%090%09100%09100%09100%0919%0910%098%0911%099%0913%09100%09100%0982%0944%0945%0931%091%091%0D%0A3%0972%09100%0966%090%09100%09100%09100%0933%098%0910%0911%098%0913%09100%09100%0985%0948%0948%0934%091%091%0D%0A3%0973%09100%0972%0931%0999%09100%0953%0919%0913%099%098%099%097%09100%09100%0989%0952%0951%0938%091%091%0D%0A3%0975%09100%0973%0934%0936%09100%0931%092%0910%0912%098%099%098%09100%09100%0992%0955%0954%0941%091%091%0D%0A3%0967%0979%0975%0936%0935%0979%0914%093%095%098%0910%099%097%09100%09100%0995%0958%0956%0944%091%091%0D%0A3%0971%0955%0967%0938%0919%0955%0911%096%093%096%099%098%098%09100%09100%0998%0961%0958%0947%091%091%0D%0A3%0941%0994%0971%0937%0990%0994%099%0910%091%093%098%095%097%09100%09100%0999%0964%0961%0949%091%091%0D%0A3%0972%09100%0941%0940%0942%09100%0918%099%093%098%096%095%099%09100%09100%09100%0967%0963%0952%091%091%0D%0A3%0962%09100%0972%0940%09100%09100%0918%097%093%0910%094%093%0910%09100%09100%09100%0970%0965%0954%091%091%0D%0A3%0966%09100%0962%0941%0957%09100%0916%099%092%099%094%092%099%09100%09100%09100%0973%0968%0957%091%091%0D%0A3%0962%0962%0966%0941%0937%0962%0912%099%093%096%094%092%098%09100%09100%09100%0975%0970%0959%091%091%0D%0A3%0963%09100%0962%0941%0979%09100%0921%094%094%094%095%093%094%09100%09100%09100%0978%0972%0961%091%091]&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;This is the raw data I was getting from the sensors.&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Screenshot-2026_2D00_10_2D00_20-194656.png" /&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;Visually&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;The ESP32 was communicating to the Radar using a serial connection at a 256,000 baud rate.&lt;/em&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;&lt;/em&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;&lt;/em&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;&lt;/em&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:d852b90d-114c-4ac4-bb8e-d75c1c47fc25:type=c_cpp&amp;text=%23define%20LD2410_BAUD%20256000%0D%0A%23define%20RX_PIN%2016%0D%0A%23define%20TX_PIN%2017%0D%0A%23define%20MAX_GATES%209%0D%0A%0D%0A%2F%2F%20Frame%20parser%20state%3A%20header%20-%3E%20length%20-%3E%20payload%20-%3E%20footer%0D%0Aenum%20%7B%20ST_HDR%2C%20ST_LEN%2C%20ST_DATA%2C%20ST_FOOT%20%7D%20rxSt%20%3D%20ST_HDR%3B%0D%0A%0D%0Avoid%20setup%28%29%20%7B%0D%0A%20%20Serial.begin%28115200%29%3B%0D%0A%20%20Serial2.begin%28LD2410_BAUD%2C%20SERIAL_8N1%2C%20RX_PIN%2C%20TX_PIN%29%3B%0D%0A%20%20%2F%2F%20Engineering%20mode%20is%20enabled%20later%20by%20sending%20%27n%27%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Non-blocking%3A%20consumes%20whatever%20bytes%20are%20waiting%20on%20UART2%0D%0Astatic%20void%20readRadar%28%29%20%7B%0D%0A%20%20while%20%28Serial2.available%28%29%29%20%7B%0D%0A%20%20%20%20uint8_t%20b%20%3D%20Serial2.read%28%29%3B%0D%0A%20%20%20%20switch%20%28rxSt%29%20%7B%0D%0A%20%20%20%20%20%20case%20ST_HDR%3A%20%20%20%2F%2A%20slide%20a%204-byte%20window%20until%20F4%20F3%20F2%20F1%20%28data%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20or%20FD%20FC%20FB%20FA%20%28command%20ACK%29%20matches%20%2A%2F%20%20%20%20%20%20break%3B%0D%0A%20%20%20%20%20%20case%20ST_LEN%3A%20%20%20%2F%2A%20read%202-byte%20little-endian%20payload%20length%20%2A%2F%20%20%20break%3B%0D%0A%20%20%20%20%20%20case%20ST_DATA%3A%20%20%2F%2A%20collect%20%27length%27%20payload%20bytes%20%2A%2F%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0D%0A%20%20%20%20%20%20case%20ST_FOOT%3A%20%20%2F%2A%20collect%20F8%20F7%20F6%20F5%2C%20then%20parseData%28%29%20%2A%2F%20%20%20%20%20%20break%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Decode%20one%20engineering-mode%20frame%20%28d%20points%20at%20the%20payload%29%0D%0Astatic%20void%20parseData%28%29%20%7B%0D%0A%20%20const%20uint8_t%2A%20d%20%3D%20fb%20%2B%206%3B%0D%0A%20%20if%20%28d%5B1%5D%20%21%3D%200xAA%29%20return%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20data%20marker%0D%0A%0D%0A%20%20g_state%20%20%20%3D%20d%5B2%5D%3B%0D%0A%20%20g_movDist%20%3D%20d%5B3%5D%20%7C%20%28d%5B4%5D%20%3C%3C%208%29%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20cm%0D%0A%20%20g_movEn%20%20%20%3D%20d%5B5%5D%3B%0D%0A%20%20g_staDist%20%3D%20d%5B6%5D%20%7C%20%28d%5B7%5D%20%3C%3C%208%29%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20cm%0D%0A%20%20g_detDist%20%3D%20d%5B9%5D%20%7C%20%28d%5B10%5D%20%3C%3C%208%29%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20cm%0D%0A%0D%0A%20%20if%20%28d%5B0%5D%20%3D%3D%200x01%29%20%7B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20engineering%20frame%0D%0A%20%20%20%20uint8_t%20idx%20%3D%2013%3B%0D%0A%20%20%20%20for%20%28uint8_t%20i%20%3D%200%3B%20i%20%3C%3D%20g_maxMov%3B%20i%2B%2B%29%20g_movG%5Bi%5D%20%3D%20d%5Bidx%2B%2B%5D%3B%20%20%2F%2F%20MG0..MG8%0D%0A%20%20%20%20for%20%28uint8_t%20i%20%3D%200%3B%20i%20%3C%3D%20g_maxSta%3B%20i%2B%2B%29%20g_staG%5Bi%5D%20%3D%20d%5Bidx%2B%2B%5D%3B%20%20%2F%2F%20SG0..SG8%0D%0A%20%20%20%20g_light%20%3D%20d%5Bidx%2B%2B%5D%3B%0D%0A%20%20%20%20g_out%20%20%20%3D%20d%5Bidx%5D%3B%0D%0A%20%20%7D%0D%0A%20%20newData%20%3D%20true%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20One%20row%20per%20frame%2C%2020%20Hz%0D%0Astatic%20void%20outputLine%28%29%20%7B%0D%0A%20%20Serial.print%28g_state%29%3B%20%20%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20Serial.print%28g_movDist%29%3B%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20Serial.print%28g_movEn%29%3B%20%20%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20Serial.print%28g_staDist%29%3B%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20Serial.print%28g_detDist%29%3B%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20for%20%28uint8_t%20i%20%3D%200%3B%20i%20%3C%20MAX_GATES%3B%20i%2B%2B%29%20%7B%20Serial.print%28g_movG%5Bi%5D%29%3B%20Serial.print%28%27%5Ct%27%29%3B%20%7D%0D%0A%20%20for%20%28uint8_t%20i%20%3D%203%3B%20i%20%3C%20MAX_GATES%3B%20i%2B%2B%29%20%7B%20Serial.print%28g_staG%5Bi%5D%29%3B%20Serial.print%28%27%5Ct%27%29%3B%20%7D%0D%0A%20%20Serial.print%28g_light%29%3B%20Serial.print%28%27%5Ct%27%29%3B%0D%0A%20%20Serial.println%28g_out%29%3B%0D%0A%7D]&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;This is the simplified version of the code I used for the ESP32 to send data over serial to the computer and also to take received data from the radar.&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt; The radar streams binary frames continuously, so the firmware runs a small state machine that finds the frame header, reads the length, collects the payload and footer, and then decodes the fields. In engineering mode each frame carries the target state, moving and static distances, the energy of each of the nine range gates, and the light and OUT pin values. The sketch prints these as one tab-separated row at 20 Hz, which is the raw data above and the input to the CSV logger. The 22 columns are State, MovDist, MovEnergy, StaDist, DetDist, MG0&amp;ndash;MG8, SG3&amp;ndash;SG8, Light and OutPin.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;The real algorithm&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:150%;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;After the failure in creating the impulse, I had to work on the algorithm.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;In order to create the algorithm first, I have to look for patterns while I&amp;#39;m breathing from the data. The best way a human can do that is visually. So I looked at the visual data.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Screenshot-2026_2D00_10_2D00_22-144519.png" /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;I recorded the data. Keeping in mind that it detects breathing while sleeping, I made as minimalist a movement as possible to detect only the movement of breathing, along with the background noise (as it will be there while sleeping also). I captured it and after staring at it for a while, I noticed some patterns.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/data.png" /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;So as you can see, while breathing we also inhale and exhale and the duration while we do that at the peaks is not that much different. In between the zeroth values, while we are moving towards the next breathing process, there is a noticeable difference that we can use to detect the n number of breaths.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&amp;nbsp;And we can also see the wave format in the crust, in the gate energies. By that&amp;nbsp; we can also detect the intensity and the distance and as per the distance we can also get the accurate value of breaths per minute.&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;And the code was very long because we have to manage the communication especially too and also while plotting it all and showing all the graphs. After implementation the code I developed was fairly long, I think really long. I won&amp;#39;t be able to write it without using AI and I think if I do I will need a week or two.If you want to take a look at it, you can see it below.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:833ea384-2682-4f41-a6ec-4a84b485596c:type=python&amp;text=%22%22%22%0D%0ALD2410%20Real-Time%20mmWave%20Monitor%20%26%20Breath%20Counter%20%28Algorithm%201%29%0D%0A%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%0D%0AConnects%20to%20ESP32%20over%20serial%2C%20prompts%20the%20user%20to%20send%20%27n%27%20to%0D%0Astart%20streaming%2C%20verifies%20incoming%20radar%20data%2C%20opens%20a%20fast%20real-time%0D%0Agraph%20window%2C%20and%20runs%20the%20breath%20counting%20algorithm%20%28Algorithm%201%29%3A%0D%0A%0D%0AAlgorithm%201%20Logic%3A%0D%0A%20%201.%20Trigger%20%2F%20Arming%3A%20Waits%20until%20moving%20energy%20is%20%3E%3D%20100%20continuously%0D%0A%20%20%20%20%20for%20the%20first%203%20seconds%20%28TRIGGER_DURATION_SEC%20%3D%203.0s%29.%0D%0A%20%202.%20Breath%20Counting%3A%20Once%20armed%2C%20counts%20breath%20pulses%20when%20non-zero%20peaks%0D%0A%20%20%20%20%20are%20separated%20by%20%3E%3D%2025%20consecutive%20zero%20values%20%28energy%20%3C%3D%20baseline%29.%0D%0A%20%203.%20Loop%20%26%20Stop%3A%20Loops%20and%20increments%20breath%20count%20until%20moving%20energy%0D%0A%20%20%20%20%20reaches%20constant%20100%20for%203%20seconds%20%28STOP_DURATION_SEC%20%3D%203.0s%29.%0D%0A%20%20%20%20%20When%20constant%20100%20for%203.0s%20is%20met%2C%20the%20session%20concludes.%0D%0A%20%20%20%20%20If%20moving%20energy%20%3E%3D%20100%20for%203.0s%20occurs%20again%2C%20it%20re-arms%20and%20loops.%0D%0A%0D%0ARequirements%3A%0D%0A%20%20%20%20pip%20install%20pyserial%20matplotlib%20numpy%0D%0A%0D%0AUsage%3A%0D%0A%20%20%20%20python%20ld2410_plotter.py%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20auto-detect%20port%0D%0A%20%20%20%20python%20ld2410_plotter.py%20COM3%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20specific%20port%0D%0A%20%20%20%20python%20ld2410_plotter.py%20COM3%20--auto-start%20%20%20%20%23%20send%20%27n%27%20without%20prompt%0D%0A%0D%0AControls%20in%20Plot%20Window%3A%0D%0A%20%20%20%20%5B%2B%5D%20%2F%20%5B%3D%5D%20%2F%20%5BUP%5D%20%20%3A%20Increase%20Breath%20Baseline%20noise%20floor%20live%20%28%2B1%29%0D%0A%20%20%20%20%5B-%5D%20%2F%20%5B_%5D%20%2F%20%5BDOWN%5D%3A%20Decrease%20Breath%20Baseline%20noise%20floor%20live%20%28-1%29%0D%0A%20%20%20%20%5BMouse%20Drag%5D%20%20%20%20%20%20%3A%20Drag%20the%20%27Baseline%20%28Noise%20Floor%29%27%20slider%20at%20the%20bottom%0D%0A%20%20%20%20%5Bb%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Reset%20%2F%20Re-arm%20Breath%20Counter%0D%0A%20%20%20%20%5Bn%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Send%20%27n%27%20%28Start%20%2F%20Resume%20streaming%29%0D%0A%20%20%20%20%5Bx%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Send%20%27x%27%20%28Pause%20streaming%29%0D%0A%20%20%20%20%5B1%5D%20-%20%5B5%5D%20%20%20%20%20%20%20%20%20%3A%20Set%20sensitivity%20presets%20%281%3D5%2F5%20...%205%3D75%2F75%29%0D%0A%20%20%20%20%5Bv%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Read%20firmware%20version%0D%0A%20%20%20%20%5Bp%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Read%20radar%20parameters%0D%0A%20%20%20%20%5Bq%5D%20or%20%5BESC%5D%20%20%20%20%20%20%3A%20Quit%0D%0A%22%22%22%0D%0A%0D%0Aimport%20sys%0D%0Aimport%20os%0D%0Aimport%20time%0D%0Aimport%20argparse%0D%0Aimport%20threading%0D%0Aimport%20collections%0D%0A%0D%0Aimport%20serial%0D%0Aimport%20serial.tools.list_ports%0D%0Aimport%20numpy%20as%20np%0D%0Aimport%20matplotlib%0D%0Aimport%20matplotlib.pyplot%20as%20plt%0D%0Aimport%20matplotlib.animation%20as%20animation%0D%0Afrom%20matplotlib.gridspec%20import%20GridSpec%0D%0Afrom%20matplotlib.widgets%20import%20Slider%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20USER%20CONFIGURATION%20%28ALTER%20VARIABLES%20HERE%29%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%23%20You%20can%20freely%20alter%20any%20of%20these%20variables%20directly%20to%20adjust%20radar%20and%20breath%20detection%20behavior%3A%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%201.%20Breath%20Detection%20%26%20Baseline%20Tuning%20%E2%94%80%E2%94%80%0D%0ASAMPLE_BASELINE%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%2015.0%20%20%20%20%20%23%20Breath%20detection%20noise%20floor%20%2F%20baseline%20%28Moving%20Energy%200-100%29.%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Values%20%3C%3D%20SAMPLE_BASELINE%20are%20treated%20as%20silence%2Fpause%20%28zeros%29%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Values%20%3E%20SAMPLE_BASELINE%20are%20detected%20as%20breathing%20motion%20peaks.%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20NOTE%3A%20You%20can%20also%20adjust%20this%20LIVE%20in%20the%20plot%20window%20via%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20the%20on-screen%20slider%20or%20%5B%2B%5D%20%2F%20%5B-%5D%20%2F%20%5BUP%5D%20%2F%20%5BDOWN%5D%20keys%21%0D%0A%0D%0AMIN_ZERO_GAP%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%20%2015.0%0D%0AMIN_ZERO_GAP%20%20%3D%20SAMPLE_BASELINE%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Minimum%20consecutive%20zero%20values%20required%20between%20peaks%20%28default%3A%2025%29.%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Difference%20between%20two%20peaks%20is%20%3E%2025%20and%20all%2025%20or%20more%20values%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20in%20between%20are%20zero%20%28%3C%3D%20SAMPLE_BASELINE%29.%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%202.%20Trigger%20%26%20Stop%20Durations%20%283s%20at%20100%20Trigger%20%26%20Exit%20Loop%29%20%E2%94%80%E2%94%80%0D%0ATRIGGER_ENERGY_REQ%20%20%20%20%20%20%20%20%20%20%20%3D%20100.0%20%20%20%20%23%20Energy%20required%20to%20arm%20%28moving%20energy%20not%20less%20than%20100%29.%0D%0ATRIGGER_DURATION_SEC%20%20%20%20%20%20%20%20%20%3D%203.0%20%20%20%20%20%20%23%20Moving%20energy%20must%20be%20%3E%3D%20100%20for%20first%203%20seconds%20to%20start%20counting.%0D%0ASTOP_DURATION_SEC%20%20%20%20%20%20%20%20%20%20%20%20%3D%203.0%20%20%20%20%20%20%23%20Loops%20until%20moving%20energy%20reaches%20constant%20100%20for%203%20seconds%20to%20stop.%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%203.%20Radar%20%26%20Display%20Window%20Settings%20%E2%94%80%E2%94%80%0D%0ABAUD_RATE%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%20115200%20%20%20%23%20Serial%20communication%20baud%20rate%20%28ESP32%20defaults%20to%20115200%29.%0D%0AHISTORY%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%20200%20%20%20%20%20%20%23%20Rolling%20waveform%20buffer%20size%20%28200%20samples%20%40%2020Hz%20%3D%2010.0%20seconds%29.%0D%0AUPDATE_MS%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%2040%20%20%20%20%20%20%20%23%20Matplotlib%20GUI%20refresh%20interval%20in%20milliseconds%20%28~25%20FPS%29.%0D%0ADARK_BG%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%20True%20%20%20%20%20%23%20True%3A%20Dark%20mode%20%28%23111111%29%3B%20False%3A%20Light%20mode.%0D%0AGATE_DIST_M%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%200.75%20%20%20%20%20%23%20Radar%20gate%20resolution%20in%20metres%20%280.75m%20per%20gate%29.%0D%0AMAX_GATES%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3D%209%20%20%20%20%20%20%20%20%23%20Total%20number%20of%20distance%20gates%20%280%20to%208%29.%0D%0A%0D%0A%23%20Column%20indices%20%280-based%29%20for%20the%2022%20columns%20received%20from%20ESP32%0D%0ACOL_STATE%20%20%20%20%3D%200%0D%0ACOL_MOVDIST%20%20%3D%201%0D%0ACOL_MOVEN%20%20%20%20%3D%202%0D%0ACOL_STADIST%20%20%3D%203%0D%0ACOL_DETDIST%20%20%3D%204%0D%0ACOL_MG_START%20%3D%205%20%20%20%20%20%20%20%20%20%23%20MG0..MG8%20%20%E2%86%92%20indices%205..13%0D%0ACOL_SG_START%20%3D%2014%20%20%20%20%20%20%20%20%23%20SG3..SG8%20%20%E2%86%92%20indices%2014..19%0D%0ACOL_LIGHT%20%20%20%20%3D%2020%0D%0ACOL_OUTPIN%20%20%20%3D%2021%0D%0ATOTAL_COLS%20%20%20%3D%2022%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Serial%20auto-detect%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Adef%20find_esp32_port%28%29%3A%0D%0A%20%20%20%20%22%22%22Return%20the%20most%20likely%20ESP32%20COM%20port%2C%20or%20None.%22%22%22%0D%0A%20%20%20%20ESP_VIDS%20%3D%20%7B0x10C4%2C%200x1A86%2C%200x0403%7D%20%20%20%23%20CP2102%2C%20CH340%2C%20FTDI%0D%0A%20%20%20%20ports%20%3D%20serial.tools.list_ports.comports%28%29%0D%0A%20%20%20%20for%20p%20in%20ports%3A%0D%0A%20%20%20%20%20%20%20%20if%20p.vid%20in%20ESP_VIDS%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20p.device%0D%0A%20%20%20%20if%20ports%3A%0D%0A%20%20%20%20%20%20%20%20return%20ports%5B0%5D.device%0D%0A%20%20%20%20return%20None%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Breath%20Counter%20%28Algorithm%201%29%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Aclass%20BreathCounter%3A%0D%0A%20%20%20%20%22%22%22%0D%0A%20%20%20%20Real-Time%20Breath%20Counter%20Algorithm%20%28Algorithm%201%29%3A%0D%0A%20%20%20%201.%20Arming%20%2F%20Trigger%3A%0D%0A%20%20%20%20%20%20%20-%20Requires%20Moving%20Energy%20%3E%3D%20100%20for%20the%20first%203.0%20seconds%20to%20start%20counting.%0D%0A%20%20%20%202.%20Peak%20%26%20Zero-Crossing%20Detection%3A%0D%0A%20%20%20%20%20%20%20-%20Once%20armed%2C%20increments%20breath%20count%20when%20two%20non-zero%20peaks%20are%20separated%0D%0A%20%20%20%20%20%20%20%20%20by%20%3E%3D%2025%20consecutive%20zero%20values%20%28moving%20energy%20%3C%3D%20SAMPLE_BASELINE%29.%0D%0A%20%20%20%203.%20Loop%20%26%20Session%20Stop%3A%0D%0A%20%20%20%20%20%20%20-%20Loops%20and%20continues%20counting%20until%20moving%20energy%20reaches%20constant%20100%0D%0A%20%20%20%20%20%20%20%20%20for%203.0%20continuous%20seconds%20to%20stop.%0D%0A%20%20%20%20%20%20%20-%20Re-arms%20if%20moving%20energy%20%3E%3D%20100%20for%203.0s%20occurs%20again.%0D%0A%20%20%20%20%22%22%22%0D%0A%20%20%20%20STATE_WAITING%20%3D%20%22WAITING_TRIGGER%22%20%20%23%20Waiting%20for%20%3E%3D%20100%20for%203s%0D%0A%20%20%20%20STATE_ACTIVE%20%20%3D%20%22COUNTING%22%20%20%20%20%20%20%20%20%20%23%20Active%20breath%20counting%20session%0D%0A%20%20%20%20STATE_STOPPED%20%3D%20%22STOPPED%22%20%20%20%20%20%20%20%20%20%20%23%20Stopped%20after%20constant%20100%20for%203s%0D%0A%0D%0A%20%20%20%20def%20__init__%28self%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20trigger_duration%3DTRIGGER_DURATION_SEC%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stop_duration%3DSTOP_DURATION_SEC%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20min_zero_gap%3DMIN_ZERO_GAP%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20min_trigger_energy%3DTRIGGER_ENERGY_REQ%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20peak_threshold%3DSAMPLE_BASELINE%29%3A%0D%0A%20%20%20%20%20%20%20%20self.trigger_duration%20%3D%20float%28trigger_duration%29%0D%0A%20%20%20%20%20%20%20%20self.stop_duration%20%3D%20float%28stop_duration%29%0D%0A%20%20%20%20%20%20%20%20self.min_zero_gap%20%3D%20int%28min_zero_gap%29%0D%0A%20%20%20%20%20%20%20%20self.min_trigger_energy%20%3D%20float%28min_trigger_energy%29%0D%0A%20%20%20%20%20%20%20%20self.peak_threshold%20%3D%20float%28peak_threshold%29%0D%0A%0D%0A%20%20%20%20%20%20%20%20self.state%20%3D%20self.STATE_WAITING%0D%0A%20%20%20%20%20%20%20%20self.breath_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.in_breath%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20self.has_peak%20%3D%20False%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20Timers%20%26%20arming%20flags%0D%0A%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.stop_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.waiting_to_clear_trigger%20%3D%20False%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20Progress%20tracking%0D%0A%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20self.stop_progress%20%3D%200.0%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20Diagnostics%20%26%20BPM%0D%0A%20%20%20%20%20%20%20%20self.last_breath_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.last_peak_val%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20self.last_zero_interval%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.bpm%20%3D%200.0%0D%0A%0D%0A%20%20%20%20def%20reset%28self%29%3A%0D%0A%20%20%20%20%20%20%20%20%22%22%22Reset%20counter%20back%20to%20waiting%20for%20trigger.%22%22%22%0D%0A%20%20%20%20%20%20%20%20self.state%20%3D%20self.STATE_WAITING%0D%0A%20%20%20%20%20%20%20%20self.breath_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.in_breath%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20self.has_peak%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.stop_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.waiting_to_clear_trigger%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20self.stop_progress%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20self.last_breath_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20self.last_peak_val%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20self.last_zero_interval%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.bpm%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20print%28f%22%5Cn%5BBREATH%5D%20Counter%20reset.%20Waiting%20for%20Moving%20Energy%20%3E%3D%20%7Bself.min_trigger_energy%3A.0f%7D%20for%20%7Bself.trigger_duration%3A.1f%7Ds...%5Cn%22%29%0D%0A%0D%0A%20%20%20%20def%20process_sample%28self%2C%20moving_energy%3A%20float%2C%20timestamp%3A%20float%20%3D%20None%29%20-%3E%20bool%3A%0D%0A%20%20%20%20%20%20%20%20%22%22%22%0D%0A%20%20%20%20%20%20%20%20Process%20incoming%20radar%20moving%20energy%20sample.%0D%0A%20%20%20%20%20%20%20%20Returns%20True%20if%20a%20new%20breath%20peak%20was%20detected%20on%20this%20sample%2C%20False%20otherwise.%0D%0A%20%20%20%20%20%20%20%20%22%22%22%0D%0A%20%20%20%20%20%20%20%20if%20timestamp%20is%20None%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20timestamp%20%3D%20time.time%28%29%0D%0A%0D%0A%20%20%20%20%20%20%20%20is_breath%20%3D%20False%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20%E2%94%80%E2%94%80%20State%201%3A%20WAITING_TRIGGER%20%E2%94%80%E2%94%80%0D%0A%20%20%20%20%20%20%20%20if%20self.state%20%3D%3D%20self.STATE_WAITING%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20moving_energy%20%3E%3D%20self.min_trigger_energy%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.start_100_time%20is%20None%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20timestamp%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20elapsed%20%3D%20timestamp%20-%20self.start_100_time%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%20min%281.0%2C%20elapsed%20%2F%20self.trigger_duration%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20elapsed%20%3E%3D%20%28self.trigger_duration%20-%201e-4%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.state%20%3D%20self.STATE_ACTIVE%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.breath_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.has_peak%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.waiting_to_clear_trigger%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%201.0%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5Cn%5BBREATH%5D%20%3E%3E%3E%20TRIGGER%20MET%21%20Moving%20Energy%20was%20%3E%3D%20%7Bself.min_trigger_energy%3A.0f%7D%20for%20%7Bself.trigger_duration%3A.1f%7Ds.%20Breath%20counting%20ACTIVE%21%20%3C%3C%3C%5Cn%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%200.0%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20%E2%94%80%E2%94%80%20State%202%3A%20COUNTING%20%E2%94%80%E2%94%80%0D%0A%20%20%20%20%20%20%20%20elif%20self.state%20%3D%3D%20self.STATE_ACTIVE%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Check%20stop%20condition%3A%20constant%20100%20for%20stop_duration%20%283.0s%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20moving_energy%20%3E%3D%20self.min_trigger_energy%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20self.waiting_to_clear_trigger%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.stop_100_time%20is%20None%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_100_time%20%3D%20timestamp%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20elapsed%20%3D%20timestamp%20-%20self.stop_100_time%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_progress%20%3D%20min%281.0%2C%20elapsed%20%2F%20self.stop_duration%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20elapsed%20%3E%3D%20%28self.stop_duration%20-%201e-4%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.state%20%3D%20self.STATE_STOPPED%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_progress%20%3D%201.0%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5Cn%5BBREATH%5D%20%3E%3E%3E%20SESSION%20COMPLETED%21%20Constant%20%7Bself.min_trigger_energy%3A.0f%7D%20for%20%7Bself.stop_duration%3A.1f%7Ds%20reached.%20Final%20Breath%20Count%3A%20%7Bself.breath_count%7D%20%3C%3C%3C%5Cn%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20False%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20While%20energy%20is%20saturated%20at%20100%2C%20do%20not%20count%20as%20a%20breath%20peak%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20False%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.waiting_to_clear_trigger%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_progress%20%3D%200.0%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Peak%20%26%20Zero-crossing%20detection%20based%20on%20baseline%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20moving_energy%20%3E%20self.peak_threshold%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20self.in_breath%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Check%20if%20preceded%20by%20%3E%3D%2025%20consecutive%20zero%20values%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.zero_count%20%3E%3D%20self.min_zero_gap%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.breath_count%20%2B%3D%201%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.in_breath%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.last_zero_interval%20%3D%20self.zero_count%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.last_peak_val%20%3D%20moving_energy%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.last_breath_time%20is%20not%20None%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20interval%20%3D%20timestamp%20-%20self.last_breath_time%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%201.0%20%3C%3D%20interval%20%3C%3D%2015.0%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.bpm%20%3D%2060.0%20%2F%20interval%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.last_breath_time%20%3D%20timestamp%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20is_breath%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bpm_str%20%3D%20f%22%20%28%7Bself.bpm%3A.0f%7D%20BPM%29%22%20if%20self.bpm%20%3E%200%20else%20%22%22%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5BBREATH%5D%20%2B1%20Breath%20%23%7Bself.breath_count%7D%20detected%21%7Bbpm_str%7D%20%28Zeros%20in%20between%3A%20%7Bself.last_zero_interval%7D%2C%20Energy%3A%20%7Bmoving_energy%3A.0f%7D%29%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Continuing%20current%20breath%20or%20sub-pulse%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.last_peak_val%20%3D%20max%28self.last_peak_val%2C%20moving_energy%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.last_peak_val%20%3D%20max%28self.last_peak_val%2C%20moving_energy%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Value%20is%20in%20the%20baseline%20noise%20floor%20%28%3C%3D%20peak_threshold%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%2B%3D%201%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.zero_count%20%3E%3D%20self.min_zero_gap%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.in_breath%20%3D%20False%0D%0A%0D%0A%20%20%20%20%20%20%20%20%23%20%E2%94%80%E2%94%80%20State%203%3A%20STOPPED%20%E2%94%80%E2%94%80%0D%0A%20%20%20%20%20%20%20%20elif%20self.state%20%3D%3D%20self.STATE_STOPPED%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Re-arm%20loop%20if%20moving%20energy%20%3E%3D%20100%20for%203s%20again%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20moving_energy%20%3C%20self.min_trigger_energy%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.trigger_progress%20%3D%200.0%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20self.start_100_time%20is%20None%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20timestamp%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20elapsed%20%3D%20timestamp%20-%20self.start_100_time%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20elapsed%20%3E%3D%20%28self.trigger_duration%20-%201e-4%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5Cn%5BBREATH%5D%20%3E%3E%3E%20RE-TRIGGER%20MET%21%20Starting%20new%20breath%20counting%20session...%20%3C%3C%3C%5Cn%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.state%20%3D%20self.STATE_ACTIVE%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.breath_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.zero_count%20%3D%200%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.has_peak%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.waiting_to_clear_trigger%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.start_100_time%20%3D%20None%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.stop_100_time%20%3D%20None%0D%0A%0D%0A%20%20%20%20%20%20%20%20return%20is_breath%0D%0A%0D%0A%20%20%20%20def%20get_status_summary%28self%29%3A%0D%0A%20%20%20%20%20%20%20%20return%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22state%22%3A%20self.state%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22count%22%3A%20self.breath_count%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22zero_count%22%3A%20self.zero_count%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22last_interval%22%3A%20self.last_zero_interval%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22trigger_progress%22%3A%20self.trigger_progress%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22stop_progress%22%3A%20self.stop_progress%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22bpm%22%3A%20self.bpm%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%22peak_threshold%22%3A%20self.peak_threshold%0D%0A%20%20%20%20%20%20%20%20%7D%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Data%20thread%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Aclass%20RadarReader%28threading.Thread%29%3A%0D%0A%20%20%20%20%22%22%22Reads%20serial%20lines%20in%20background%20and%20feeds%20data%20to%20thread-safe%20buffer.%22%22%22%0D%0A%0D%0A%20%20%20%20def%20__init__%28self%2C%20ser%2C%20breath_counter%3DNone%29%3A%0D%0A%20%20%20%20%20%20%20%20super%28%29.__init__%28daemon%3DTrue%29%0D%0A%20%20%20%20%20%20%20%20self.ser%20%20%20%20%20%20%20%20%20%20%20%20%3D%20ser%0D%0A%20%20%20%20%20%20%20%20self.buf%20%20%20%20%20%20%20%20%20%20%20%20%3D%20collections.deque%28maxlen%3DHISTORY%29%0D%0A%20%20%20%20%20%20%20%20self.breath_flags%20%20%20%3D%20collections.deque%28maxlen%3DHISTORY%29%0D%0A%20%20%20%20%20%20%20%20self.lock%20%20%20%20%20%20%20%20%20%20%20%3D%20threading.Lock%28%29%0D%0A%20%20%20%20%20%20%20%20self.status%20%20%20%20%20%20%20%20%20%3D%20%22Streaming%22%0D%0A%20%20%20%20%20%20%20%20self.connected%20%20%20%20%20%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20self._stop%20%20%20%20%20%20%20%20%20%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20self.latest%20%20%20%20%20%20%20%20%20%3D%20%5B0.0%5D%20%2A%20TOTAL_COLS%0D%0A%20%20%20%20%20%20%20%20self.packet_cnt%20%20%20%20%20%3D%200%0D%0A%20%20%20%20%20%20%20%20self.breath_counter%20%3D%20breath_counter%20or%20BreathCounter%28%29%0D%0A%0D%0A%20%20%20%20def%20stop%28self%29%3A%0D%0A%20%20%20%20%20%20%20%20self._stop%20%3D%20True%0D%0A%0D%0A%20%20%20%20def%20send%28self%2C%20cmd%3A%20str%29%3A%0D%0A%20%20%20%20%20%20%20%20%22%22%22Send%20a%20single%20character%20command%20to%20ESP32.%22%22%22%0D%0A%20%20%20%20%20%20%20%20if%20self.ser%20and%20self.ser.is_open%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20try%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.ser.write%28%28cmd%20%2B%20%27%5Cn%27%29.encode%28%27ascii%27%29%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.ser.flush%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20except%20Exception%20as%20e%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5BSERIAL%5D%20Write%20error%3A%20%7Be%7D%22%29%0D%0A%0D%0A%20%20%20%20def%20run%28self%29%3A%0D%0A%20%20%20%20%20%20%20%20while%20not%20self._stop%20and%20self.ser%20and%20self.ser.is_open%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20try%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20raw%20%3D%20self.ser.readline%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20raw%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%20%3D%20raw.decode%28%27ascii%27%2C%20errors%3D%27ignore%27%29.strip%28%29%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20line%20or%20line.startswith%28%27%23%27%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20line.startswith%28%27%23%27%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Display%20firmware%20%2F%20parameters%20feedback%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20any%28k%20in%20line%20for%20k%20in%20%28%22FW%22%2C%20%22Params%22%2C%20%22Sens%22%2C%20%22MaxDist%22%2C%20%22STREAMING%22%29%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%5BESP32%5D%20%7Bline%7D%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Data%20line%20starts%20with%20numeric%20State%20%280%2C%201%2C%202%2C%203%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20line%5B0%5D.isdigit%28%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sep%20%3D%20%27%2C%27%20if%20%27%2C%27%20in%20line%20else%20%27%5Ct%27%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20parts%20%3D%20line.split%28sep%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20len%28parts%29%20%21%3D%20TOTAL_COLS%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20row%20%3D%20%5Bfloat%28p%29%20for%20p%20in%20parts%5D%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20is_breath%20%3D%20self.breath_counter.process_sample%28row%5BCOL_MOVEN%5D%29%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20with%20self.lock%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.buf.append%28row%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.breath_flags.append%28is_breath%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.latest%20%3D%20row%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.packet_cnt%20%2B%3D%201%0D%0A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20except%20serial.SerialException%20as%20e%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.status%20%3D%20f%22Serial%20error%3A%20%7Be%7D%22%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.connected%20%3D%20False%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20except%20Exception%20as%20e%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.status%20%3D%20f%22Error%3A%20%7Be%7D%22%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Plot%20Setup%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0ASTATE_LABELS%20%3D%20%7B0%3A%20%22No%20Target%22%2C%201%3A%20%22Moving%22%2C%202%3A%20%22Static%22%2C%203%3A%20%22Both%22%7D%0D%0ASTATE_COLORS%20%3D%20%7B0%3A%20%27%23555555%27%2C%201%3A%20%27%2300ccff%27%2C%202%3A%20%27%23ffaa00%27%2C%203%3A%20%27%23ff4444%27%7D%0D%0A%0D%0Aif%20DARK_BG%3A%0D%0A%20%20%20%20plt.style.use%28%27dark_background%27%29%0D%0A%0D%0Afig%20%3D%20plt.figure%28figsize%3D%2815%2C%209.5%29%2C%20facecolor%3D%27%23111111%27%20if%20DARK_BG%20else%20%27%23f5f5f5%27%29%0D%0Afig.canvas.manager.set_window_title%28%27HLK-LD2410%20Real-Time%20mmWave%20Monitor%20%26%20Breath%20Counter%27%29%0D%0A%0D%0Ags%20%3D%20GridSpec%284%2C%202%2C%20figure%3Dfig%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20left%3D0.06%2C%20right%3D0.97%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20top%3D0.92%2C%20bottom%3D0.075%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20hspace%3D0.52%2C%20wspace%3D0.30%29%0D%0A%0D%0Aax_dist%20%20%20%3D%20fig.add_subplot%28gs%5B0%2C%20%3A%5D%29%20%20%20%20%23%20distances%20%E2%80%94%20full%20width%0D%0Aax_moven%20%20%3D%20fig.add_subplot%28gs%5B1%2C%20%3A%5D%29%20%20%20%20%23%20moving%20energy%20%2B%20target%20state%20%2B%20breath%0D%0Aax_movg%20%20%20%3D%20fig.add_subplot%28gs%5B2%2C%20%3A%5D%29%20%20%20%20%23%20per-gate%20moving%20energies%20MG0%E2%80%93MG8%0D%0Aax_stag%20%20%20%3D%20fig.add_subplot%28gs%5B3%2C%200%5D%29%20%20%20%20%23%20static%20gate%20energies%20SG3%E2%80%93SG8%0D%0Aax_misc%20%20%20%3D%20fig.add_subplot%28gs%5B3%2C%201%5D%29%20%20%20%20%23%20Light%20%2B%20Output%20pin%0D%0A%0D%0Adef%20style_ax%28ax%2C%20title%2C%20ylabel%3D%27%27%2C%20ylim%3DNone%29%3A%0D%0A%20%20%20%20ax.set_title%28title%2C%20fontsize%3D9.5%2C%20pad%3D3%2C%20fontweight%3D%27bold%27%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3D%27%23dddddd%27%20if%20DARK_BG%20else%20%27%23222222%27%29%0D%0A%20%20%20%20ax.set_ylabel%28ylabel%2C%20fontsize%3D8%29%0D%0A%20%20%20%20ax.tick_params%28labelsize%3D7.5%29%0D%0A%20%20%20%20ax.grid%28True%2C%20alpha%3D0.25%2C%20linestyle%3D%27%3A%27%29%0D%0A%20%20%20%20if%20ylim%3A%20ax.set_ylim%28ylim%29%0D%0A%0D%0Astyle_ax%28ax_dist%2C%20%20%27Distance%20Tracking%20%28cm%29%27%2C%20%20%20%20%20%20%20%20%20%20%20%20%27cm%27%2C%20%20%20%20%20%280%2C%20700%29%29%0D%0Astyle_ax%28ax_moven%2C%20%27Motion%20Energy%20%280-100%29%20%26%20Breath%20Detection%20%28Algorithm%201%29%27%2C%20%27Level%27%2C%20%20%280%2C%20105%29%29%0D%0Astyle_ax%28ax_movg%2C%20%20%27Moving%20Gate%20Energies%3A%20Gate%200%20to%20Gate%208%20%280.75m%20steps%29%27%2C%20%27Energy%27%2C%20%280%2C%20105%29%29%0D%0Astyle_ax%28ax_stag%2C%20%20%27Static%20Gate%20Energies%3A%20Gate%203%20to%20Gate%208%27%2C%20%27Energy%27%2C%20%280%2C%20105%29%29%0D%0Astyle_ax%28ax_misc%2C%20%20%27Photosensitive%20Light%20%26%20OUT%20Pin%20Status%27%2C%20%27Value%27%29%0D%0A%0D%0Ax_data%20%3D%20list%28range%28HISTORY%29%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%20Distance%20plot%20lines%20%E2%94%80%E2%94%80%0D%0Aline_movdist%2C%20%3D%20ax_dist.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%2300d4ff%27%2C%20lw%3D1.5%2C%20label%3D%27Moving%20Target%20Dist%27%29%0D%0Aline_stadist%2C%20%3D%20ax_dist.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%23ffaa00%27%2C%20lw%3D1.5%2C%20label%3D%27Stationary%20Target%20Dist%27%29%0D%0Aline_detdist%2C%20%3D%20ax_dist.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%2344ff88%27%2C%20lw%3D1.2%2C%20ls%3D%27--%27%2C%20label%3D%27Detection%20Range%27%29%0D%0Aax_dist.legend%28fontsize%3D7.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%20Motion%20energy%20%2B%20state%20lines%20%2B%20breath%20detections%20%2B%20baseline%20%E2%94%80%E2%94%80%0D%0Aline_moven%2C%20%3D%20ax_moven.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%23ff3388%27%2C%20lw%3D1.6%2C%20label%3D%27Moving%20Energy%27%29%0D%0Abar_state%20%20%20%3D%20ax_moven.fill_between%28x_data%2C%200%2C%20%5B0%5D%2AHISTORY%2C%20alpha%3D0.18%2C%20color%3D%27%2300aaff%27%2C%20label%3D%27State%20Area%27%29%0D%0Aline_baseline%20%3D%20ax_moven.axhline%28%0D%0A%20%20%20%20y%3DSAMPLE_BASELINE%2C%20color%3D%27%23ffee00%27%2C%20linestyle%3D%27--%27%2C%0D%0A%20%20%20%20linewidth%3D1.3%2C%20alpha%3D0.85%2C%20label%3Df%27Baseline%20%28%7BSAMPLE_BASELINE%3A.0f%7D%29%27%2C%20zorder%3D4%0D%0A%29%0D%0Aline_breaths%2C%20%3D%20ax_moven.plot%28%5B%5D%2C%20%5B%5D%2C%20%27o%27%2C%20color%3D%27%2300ffff%27%2C%20markersize%3D7.5%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20markeredgecolor%3D%27%23ffffff%27%2C%20markeredgewidth%3D1.2%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%27Breath%20Peak%20Detected%27%2C%20zorder%3D5%29%0D%0Aax_moven.legend%28fontsize%3D7.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%29%0D%0A%0D%0A%23%20Breath%20detection%20HUD%20badge%20on%20ax_moven%0D%0Abreath_hud%20%3D%20ax_moven.text%28%0D%0A%20%20%20%200.985%2C%200.88%2C%20%27%5BBreath%5D%20Initializing...%27%2C%0D%0A%20%20%20%20transform%3Dax_moven.transAxes%2C%20ha%3D%27right%27%2C%20va%3D%27top%27%2C%0D%0A%20%20%20%20fontsize%3D9.0%2C%20fontweight%3D%27bold%27%2C%0D%0A%20%20%20%20color%3D%27%23ffaa00%27%2C%0D%0A%20%20%20%20bbox%3Ddict%28boxstyle%3D%27round%2Cpad%3D0.38%27%2C%20facecolor%3D%27%23151c28%27%20if%20DARK_BG%20else%20%27%23ffffff%27%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20edgecolor%3D%27%23ffaa00%27%2C%20alpha%3D0.9%2C%20lw%3D1.2%29%0D%0A%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%20Moving%20gate%20energies%20MG0%E2%80%93MG8%20%E2%94%80%E2%94%80%0D%0AMOVG_COLORS%20%3D%20plt.cm.turbo%28np.linspace%280.08%2C%200.92%2C%20MAX_GATES%29%29%0D%0Amovg_lines%20%20%3D%20%5B%5D%0D%0Afor%20i%20in%20range%28MAX_GATES%29%3A%0D%0A%20%20%20%20ln%2C%20%3D%20ax_movg.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3DMOVG_COLORS%5Bi%5D%2C%20lw%3D1.2%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3Df%27G%7Bi%7D%20%28%7Bi%2AGATE_DIST_M%3A.2f%7Dm%29%27%29%0D%0A%20%20%20%20movg_lines.append%28ln%29%0D%0Aax_movg.legend%28fontsize%3D6.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%2C%20ncol%3D5%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%20Static%20gate%20energies%20SG3%E2%80%93SG8%20%E2%94%80%E2%94%80%0D%0ASTAG_COLORS%20%3D%20plt.cm.cool%28np.linspace%280.15%2C%200.95%2C%206%29%29%0D%0Astag_lines%20%20%3D%20%5B%5D%0D%0Afor%20j%2C%20i%20in%20enumerate%28range%283%2C%209%29%29%3A%0D%0A%20%20%20%20ln%2C%20%3D%20ax_stag.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3DSTAG_COLORS%5Bj%5D%2C%20lw%3D1.2%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3Df%27SG%7Bi%7D%20%28%7Bi%2AGATE_DIST_M%3A.2f%7Dm%29%27%29%0D%0A%20%20%20%20stag_lines.append%28ln%29%0D%0Aax_stag.legend%28fontsize%3D6.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%2C%20ncol%3D3%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%20Light%20%26%20OutPin%20%E2%94%80%E2%94%80%0D%0Aline_light%2C%20%20%3D%20ax_misc.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%23ffee33%27%2C%20lw%3D1.3%2C%20label%3D%27Light%20Level%27%29%0D%0Aline_outpin%2C%20%3D%20ax_misc.plot%28x_data%2C%20%5B0%5D%2AHISTORY%2C%20color%3D%27%2333ffaa%27%2C%20lw%3D1.6%2C%20ls%3D%27--%27%2C%20label%3D%27OUT%20Pin%20%28Occupied%3D100%29%27%29%0D%0Aax_misc.set_ylim%280%2C%20260%29%0D%0Aax_misc.legend%28fontsize%3D7.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%29%0D%0A%0D%0A%23%20Inset%20horizontal%20snapshot%20bar%20chart%20for%20moving%20gates%0D%0Abar_ax%20%3D%20ax_movg.inset_axes%28%5B0.91%2C%200.05%2C%200.08%2C%200.90%5D%29%0D%0Abar_ax.set_xlim%280%2C%20105%29%0D%0Abar_ax.set_ylim%28-0.5%2C%20MAX_GATES%20-%200.5%29%0D%0Abar_ax.set_yticks%28range%28MAX_GATES%29%29%0D%0Abar_ax.set_yticklabels%28%5Bf%27G%7Bi%7D%27%20for%20i%20in%20range%28MAX_GATES%29%5D%2C%20fontsize%3D6%29%0D%0Abar_ax.set_xticks%28%5B%5D%29%0D%0Abar_ax.set_title%28%27Live%20G0-8%27%2C%20fontsize%3D6.5%2C%20pad%3D2%2C%20color%3D%27%23cccccc%27%20if%20DARK_BG%20else%20%27%23333333%27%29%0D%0Abar_ax.invert_yaxis%28%29%0D%0Aif%20DARK_BG%3A%20bar_ax.set_facecolor%28%27%231c1c1c%27%29%0D%0Abars%20%3D%20bar_ax.barh%28range%28MAX_GATES%29%2C%20%5B0%5D%2AMAX_GATES%2C%20color%3DMOVG_COLORS%2C%20height%3D0.65%29%0D%0A%0D%0A%23%20Top%20status%20header%20text%0D%0Astatus_txt%20%3D%20fig.text%280.5%2C%200.965%2C%20%27Connecting%E2%80%A6%27%2C%20ha%3D%27center%27%2C%20va%3D%27top%27%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D10%2C%20fontweight%3D%27bold%27%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3D%27%23aaffaa%27%20if%20DARK_BG%20else%20%27%23008800%27%29%0D%0A%0D%0A%23%20Global%20background%20reader%0D%0Areader%20%3D%20None%0D%0A%0D%0A%23%20Bottom%20interactive%20Slider%20for%20live%20Breath%20Baseline%20Noise%20Floor%20adjustment%0D%0Aslider_ax%20%3D%20fig.add_axes%28%5B0.22%2C%200.015%2C%200.56%2C%200.024%5D%2C%20facecolor%3D%27%23202020%27%20if%20DARK_BG%20else%20%27%23e8e8e8%27%29%0D%0Abaseline_slider%20%3D%20Slider%28%0D%0A%20%20%20%20slider_ax%2C%20%27Breath%20Baseline%20%28Noise%20Floor%29%3A%27%2C%0D%0A%20%20%20%200.0%2C%2070.0%2C%20valinit%3DSAMPLE_BASELINE%2C%20valstep%3D1.0%2C%0D%0A%20%20%20%20color%3D%27%2300ffcc%27%2C%0D%0A%20%20%20%20initcolor%3D%27none%27%2C%0D%0A%20%20%20%20track_color%3D%27%23141414%27%20if%20DARK_BG%20else%20%27%23cccccc%27%0D%0A%29%0D%0Abaseline_slider.label.set_color%28%27%23dddddd%27%20if%20DARK_BG%20else%20%27%23222222%27%29%0D%0Abaseline_slider.label.set_fontsize%288.5%29%0D%0Abaseline_slider.label.set_fontweight%28%27bold%27%29%0D%0Abaseline_slider.valtext.set_color%28%27%2300ffcc%27%29%0D%0Abaseline_slider.valtext.set_fontsize%288.5%29%0D%0Abaseline_slider.valtext.set_fontweight%28%27bold%27%29%0D%0A%0D%0Adef%20on_baseline_slider%28val%29%3A%0D%0A%20%20%20%20if%20reader%20and%20reader.breath_counter%3A%0D%0A%20%20%20%20%20%20%20%20reader.breath_counter.peak_threshold%20%3D%20float%28val%29%0D%0A%20%20%20%20line_baseline.set_ydata%28%5Bval%2C%20val%5D%29%0D%0A%20%20%20%20line_baseline.set_label%28f%27Baseline%20%28%7Bval%3A.0f%7D%29%27%29%0D%0A%20%20%20%20ax_moven.legend%28fontsize%3D7.5%2C%20loc%3D%27upper%20left%27%2C%20framealpha%3D0.35%29%0D%0A%20%20%20%20fig.canvas.draw_idle%28%29%0D%0A%0D%0Abaseline_slider.on_changed%28on_baseline_slider%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Animation%20Update%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Adef%20update%28_frame%29%3A%0D%0A%20%20%20%20global%20bar_state%0D%0A%0D%0A%20%20%20%20with%20reader.lock%3A%0D%0A%20%20%20%20%20%20%20%20data%20%3D%20list%28reader.buf%29%0D%0A%20%20%20%20%20%20%20%20b_flags%20%3D%20list%28reader.breath_flags%29%0D%0A%20%20%20%20%20%20%20%20latest%20%3D%20list%28reader.latest%29%0D%0A%20%20%20%20%20%20%20%20b_status%20%3D%20reader.breath_counter.get_status_summary%28%29%0D%0A%0D%0A%20%20%20%20state%20%20%3D%20int%28latest%5BCOL_STATE%5D%29%20if%20latest%20else%200%0D%0A%20%20%20%20slabel%20%3D%20STATE_LABELS.get%28state%2C%20%27Unknown%27%29%0D%0A%20%20%20%20b_cnt%20%20%3D%20b_status%5B%27count%27%5D%0D%0A%20%20%20%20b_st%20%20%20%3D%20b_status%5B%27state%27%5D%0D%0A%0D%0A%20%20%20%20%23%20Update%20top%20status%20text%0D%0A%20%20%20%20status_txt.set_text%28%0D%0A%20%20%20%20%20%20%20%20f%22State%3A%20%7Bslabel%7D%20%20%E2%94%82%20%20%22%0D%0A%20%20%20%20%20%20%20%20f%22%5BBreath%5D%20Count%3A%20%7Bb_cnt%7D%20%5B%7Bb_st%7D%5D%20%20%E2%94%82%20%20%22%0D%0A%20%20%20%20%20%20%20%20f%22Moving%3A%20%7Blatest%5BCOL_MOVDIST%5D%3A.0f%7Dcm%20%28E%3A%7Blatest%5BCOL_MOVEN%5D%3A.0f%7D%29%20%20%E2%94%82%20%20%22%0D%0A%20%20%20%20%20%20%20%20f%22Static%3A%20%7Blatest%5BCOL_STADIST%5D%3A.0f%7Dcm%20%20%E2%94%82%20%20%22%0D%0A%20%20%20%20%20%20%20%20f%22Detect%3A%20%7Blatest%5BCOL_DETDIST%5D%3A.0f%7Dcm%20%20%E2%94%82%20%20%22%0D%0A%20%20%20%20%20%20%20%20f%22OUT%3A%20%7B%27%E2%96%A0%20OCCUPIED%27%20if%20latest%5BCOL_OUTPIN%5D%20else%20%27%E2%96%A1%20Empty%27%7D%22%0D%0A%20%20%20%20%29%0D%0A%20%20%20%20status_txt.set_color%28STATE_COLORS.get%28state%2C%20%27%23aaaaaa%27%29%20if%20reader.connected%20else%20%27%23ff4444%27%29%0D%0A%0D%0A%20%20%20%20%23%20Update%20breath%20HUD%20badge%20on%20ax_moven%0D%0A%20%20%20%20if%20b_st%20%3D%3D%20BreathCounter.STATE_WAITING%3A%0D%0A%20%20%20%20%20%20%20%20pct%20%3D%20int%28b_status%5B%27trigger_progress%27%5D%20%2A%20100%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_text%28f%22%5BBreath%5D%20WAITING%20TRIGGER%20%28Moving%20Energy%20%3E%3D%20100%20for%203s%29%3A%20%7Bpct%7D%25%5CnBreaths%3A%20%7Bb_cnt%7D%22%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_color%28%27%23ffaa00%27%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.get_bbox_patch%28%29.set_edgecolor%28%27%23ffaa00%27%29%0D%0A%20%20%20%20elif%20b_st%20%3D%3D%20BreathCounter.STATE_ACTIVE%3A%0D%0A%20%20%20%20%20%20%20%20stop_pct%20%3D%20int%28b_status%5B%27stop_progress%27%5D%20%2A%20100%29%0D%0A%20%20%20%20%20%20%20%20stop_hint%20%3D%20f%22%20%E2%94%82%20Stop%20E%3D100%3A%20%7Bstop_pct%7D%25%22%20if%20stop_pct%20%3E%200%20else%20%22%22%0D%0A%20%20%20%20%20%20%20%20bpm_str%20%3D%20f%22%20%E2%94%82%20Rate%3A%20%7Bb_status%5B%27bpm%27%5D%3A.0f%7D%20BPM%22%20if%20b_status.get%28%27bpm%27%2C%200%29%20%3E%200%20else%20%22%22%0D%0A%20%20%20%20%20%20%20%20cur_base%20%3D%20b_status.get%28%27peak_threshold%27%2C%20SAMPLE_BASELINE%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_text%28%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%5BBreath%5D%20COUNT%3A%20%7Bb_cnt%7D%7Bbpm_str%7D%20%20%5BACTIVE%5D%5Cn%22%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Silence%3A%20%7Bb_status%5B%27zero_count%27%5D%7Dz%20%28req%20%3E%3D%2025z%29%20%E2%94%82%20Last%20Gap%3A%20%7Bb_status%5B%27last_interval%27%5D%7Dz%20%E2%94%82%20Base%3A%20%7Bcur_base%3A.0f%7D%7Bstop_hint%7D%22%0D%0A%20%20%20%20%20%20%20%20%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_color%28%27%2300ffcc%27%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.get_bbox_patch%28%29.set_edgecolor%28%27%2300ffcc%27%29%0D%0A%20%20%20%20elif%20b_st%20%3D%3D%20BreathCounter.STATE_STOPPED%3A%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_text%28%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%5BBreath%5D%20SESSION%20STOPPED%20%28Constant%20100%20for%203s%29%5Cn%22%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22FINAL%20BREATH%20COUNT%3A%20%7Bb_cnt%7D%20%20%E2%94%82%20%20Press%20%5BB%5D%20to%20Reset%20%2F%20Re-arm%22%0D%0A%20%20%20%20%20%20%20%20%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.set_color%28%27%23ff5555%27%29%0D%0A%20%20%20%20%20%20%20%20breath_hud.get_bbox_patch%28%29.set_edgecolor%28%27%23ff5555%27%29%0D%0A%0D%0A%20%20%20%20if%20not%20data%3A%0D%0A%20%20%20%20%20%20%20%20return%0D%0A%0D%0A%20%20%20%20n%20%3D%20len%28data%29%0D%0A%20%20%20%20arr%20%3D%20np.array%28data%29%0D%0A%20%20%20%20pad%20%3D%20HISTORY%20-%20n%0D%0A%0D%0A%20%20%20%20def%20padded%28col_idx%29%3A%0D%0A%20%20%20%20%20%20%20%20return%20%5B0.0%5D%2Apad%20%2B%20arr%5B%3A%2C%20col_idx%5D.tolist%28%29%0D%0A%0D%0A%20%20%20%20%23%201.%20Distances%20with%20adaptive%20Y-axis%0D%0A%20%20%20%20d_mov%20%3D%20padded%28COL_MOVDIST%29%0D%0A%20%20%20%20d_sta%20%3D%20padded%28COL_STADIST%29%0D%0A%20%20%20%20d_det%20%3D%20padded%28COL_DETDIST%29%0D%0A%20%20%20%20line_movdist.set_ydata%28d_mov%29%0D%0A%20%20%20%20line_stadist.set_ydata%28d_sta%29%0D%0A%20%20%20%20line_detdist.set_ydata%28d_det%29%0D%0A%0D%0A%20%20%20%20max_dist%20%3D%20max%28arr%5B%3A%2C%20%5BCOL_MOVDIST%2C%20COL_STADIST%2C%20COL_DETDIST%5D%5D.max%28%29%2C%2050%29%0D%0A%20%20%20%20ax_dist.set_ylim%280%2C%20max%28250%2C%20max_dist%20%2A%201.15%29%29%0D%0A%0D%0A%20%20%20%20%23%202.%20Motion%20Energy%20%26%20State%20fill%20%26%20Breath%20markers%0D%0A%20%20%20%20mov_energies%20%3D%20padded%28COL_MOVEN%29%0D%0A%20%20%20%20line_moven.set_ydata%28mov_energies%29%0D%0A%20%20%20%20bar_state.remove%28%29%0D%0A%20%20%20%20state_scaled%20%3D%20%5Bv%20%2A%2025%20for%20v%20in%20padded%28COL_STATE%29%5D%0D%0A%20%20%20%20bar_state%20%3D%20ax_moven.fill_between%28x_data%2C%200%2C%20state_scaled%2C%20alpha%3D0.18%2C%20color%3D%27%2300aaff%27%29%0D%0A%0D%0A%20%20%20%20padded_flags%20%3D%20%5BFalse%5D%20%2A%20pad%20%2B%20b_flags%0D%0A%20%20%20%20b_indices%20%3D%20%5Bidx%20for%20idx%2C%20flg%20in%20enumerate%28padded_flags%29%20if%20flg%5D%0D%0A%20%20%20%20b_vals%20%3D%20%5Bmov_energies%5Bidx%5D%20for%20idx%20in%20b_indices%5D%0D%0A%20%20%20%20line_breaths.set_data%28b_indices%2C%20b_vals%29%0D%0A%0D%0A%20%20%20%20%23%203.%20Moving%20Gate%20Energies%20MG0%E2%80%93MG8%0D%0A%20%20%20%20for%20i%2C%20ln%20in%20enumerate%28movg_lines%29%3A%0D%0A%20%20%20%20%20%20%20%20ln.set_ydata%28padded%28COL_MG_START%20%2B%20i%29%29%0D%0A%0D%0A%20%20%20%20if%20n%20%3E%200%3A%0D%0A%20%20%20%20%20%20%20%20snap%20%3D%20arr%5B-1%2C%20COL_MG_START%3ACOL_MG_START%2BMAX_GATES%5D%0D%0A%20%20%20%20%20%20%20%20for%20bar%2C%20val%20in%20zip%28bars%2C%20snap%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20bar.set_width%28val%29%0D%0A%0D%0A%20%20%20%20%23%204.%20Static%20Gate%20Energies%20SG3%E2%80%93SG8%0D%0A%20%20%20%20for%20j%2C%20ln%20in%20enumerate%28stag_lines%29%3A%0D%0A%20%20%20%20%20%20%20%20ln.set_ydata%28padded%28COL_SG_START%20%2B%20j%29%29%0D%0A%0D%0A%20%20%20%20%23%205.%20Light%20%26%20OUT%20Pin%0D%0A%20%20%20%20line_light.set_ydata%28padded%28COL_LIGHT%29%29%0D%0A%20%20%20%20outpin_scaled%20%3D%20%5Bv%20%2A%20100%20for%20v%20in%20padded%28COL_OUTPIN%29%5D%0D%0A%20%20%20%20line_outpin.set_ydata%28outpin_scaled%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Keyboard%20Handler%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Adef%20on_key%28event%29%3A%0D%0A%20%20%20%20global%20reader%0D%0A%20%20%20%20if%20not%20event.key%3A%0D%0A%20%20%20%20%20%20%20%20return%0D%0A%0D%0A%20%20%20%20k%20%3D%20event.key%0D%0A%0D%0A%20%20%20%20%23%20Live%20baseline%20threshold%20adjustment%0D%0A%20%20%20%20if%20k%20in%20%28%27%2B%27%2C%20%27%3D%27%2C%20%27up%27%29%3A%0D%0A%20%20%20%20%20%20%20%20new_val%20%3D%20min%2870.0%2C%20baseline_slider.val%20%2B%201.0%29%0D%0A%20%20%20%20%20%20%20%20baseline_slider.set_val%28new_val%29%0D%0A%20%20%20%20%20%20%20%20print%28f%22%5BBASELINE%5D%20Increased%20to%20%7Bnew_val%3A.0f%7D%22%29%0D%0A%20%20%20%20%20%20%20%20return%0D%0A%20%20%20%20elif%20k%20in%20%28%27-%27%2C%20%27_%27%2C%20%27down%27%29%3A%0D%0A%20%20%20%20%20%20%20%20new_val%20%3D%20max%280.0%2C%20baseline_slider.val%20-%201.0%29%0D%0A%20%20%20%20%20%20%20%20baseline_slider.set_val%28new_val%29%0D%0A%20%20%20%20%20%20%20%20print%28f%22%5BBASELINE%5D%20Decreased%20to%20%7Bnew_val%3A.0f%7D%22%29%0D%0A%20%20%20%20%20%20%20%20return%0D%0A%0D%0A%20%20%20%20%23%20Reset%20%2F%20Re-arm%20breath%20counter%0D%0A%20%20%20%20if%20k%20in%20%28%27b%27%2C%20%27B%27%29%3A%0D%0A%20%20%20%20%20%20%20%20reader.breath_counter.reset%28%29%0D%0A%20%20%20%20%20%20%20%20with%20reader.lock%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20reader.breath_flags.clear%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20reader.breath_flags.extend%28%5BFalse%5D%20%2A%20len%28reader.buf%29%29%0D%0A%20%20%20%20%20%20%20%20return%0D%0A%0D%0A%20%20%20%20%23%20Relay%20commands%20directly%20to%20ESP32%0D%0A%20%20%20%20if%20k%20in%20%28%27n%27%2C%20%27x%27%2C%20%271%27%2C%20%272%27%2C%20%273%27%2C%20%274%27%2C%20%275%27%2C%20%27v%27%2C%20%27p%27%2C%20%27d%27%2C%20%27H%27%29%3A%0D%0A%20%20%20%20%20%20%20%20reader.send%28k%29%0D%0A%20%20%20%20%20%20%20%20print%28f%22%5BCMD%5D%20Sent%20%27%7Bk%7D%27%20to%20ESP32%22%29%0D%0A%20%20%20%20elif%20k%20in%20%28%27q%27%2C%20%27escape%27%29%3A%0D%0A%20%20%20%20%20%20%20%20print%28%22%5BEXIT%5D%20Closing%20window...%22%29%0D%0A%20%20%20%20%20%20%20%20plt.close%28fig%29%0D%0A%0D%0Afig.canvas.mpl_connect%28%27key_press_event%27%2C%20on_key%29%0D%0A%0D%0A%23%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%20Main%20Entry%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0D%0A%0D%0Adef%20main%28%29%3A%0D%0A%20%20%20%20global%20reader%0D%0A%0D%0A%20%20%20%20parser%20%3D%20argparse.ArgumentParser%28description%3D%27HLK-LD2410%20Live%20Plotter%20%26%20Breath%20Counter%20%28Algorithm%201%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27port%27%2C%20nargs%3D%27%3F%27%2C%20default%3DNone%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3D%27Serial%20port%20%28e.g.%20COM3%20or%20%2Fdev%2FttyUSB0%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--baud%27%2C%20type%3Dint%2C%20default%3DBAUD_RATE%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3Df%27Baud%20rate%20%28default%20%7BBAUD_RATE%7D%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--auto-start%27%2C%20action%3D%27store_true%27%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3D%27Send%20%22n%22%20without%20waiting%20for%20manual%20console%20Enter%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--trigger-sec%27%2C%20type%3Dfloat%2C%20default%3DTRIGGER_DURATION_SEC%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3Df%27Seconds%20of%20moving%20energy%20%3E%3D%20100%20to%20start%20breath%20counting%20%28default%3A%20%7BTRIGGER_DURATION_SEC%7D%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--stop-sec%27%2C%20type%3Dfloat%2C%20default%3DSTOP_DURATION_SEC%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3Df%27Seconds%20of%20constant%20100%20moving%20energy%20to%20stop%20breath%20counting%20%28default%3A%20%7BSTOP_DURATION_SEC%7D%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--min-zeros%27%2C%20type%3Dint%2C%20default%3DMIN_ZERO_GAP%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3Df%27Minimum%20consecutive%20zero%20values%20between%20peaks%20%28default%3A%20%7BMIN_ZERO_GAP%7D%29%27%29%0D%0A%20%20%20%20parser.add_argument%28%27--peak-thresh%27%2C%20type%3Dfloat%2C%20default%3DSAMPLE_BASELINE%2C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20help%3Df%27Minimum%20moving%20energy%20noise%20floor%20%2F%20baseline%20%28default%3A%20%7BSAMPLE_BASELINE%7D%29%27%29%0D%0A%20%20%20%20args%20%3D%20parser.parse_args%28%29%0D%0A%0D%0A%20%20%20%20port%20%3D%20args.port%20or%20find_esp32_port%28%29%0D%0A%20%20%20%20if%20not%20port%3A%0D%0A%20%20%20%20%20%20%20%20print%28%22ERROR%3A%20No%20serial%20port%20detected.%20Connect%20the%20ESP32%20or%20specify%20port%20%28e.g.%20COM3%29.%22%29%0D%0A%20%20%20%20%20%20%20%20print%28%22Available%20ports%3A%22%2C%20%5Bp.device%20for%20p%20in%20serial.tools.list_ports.comports%28%29%5D%29%0D%0A%20%20%20%20%20%20%20%20sys.exit%281%29%0D%0A%0D%0A%20%20%20%20print%28%22%5Cn%22%20%2B%20%22%3D%22%20%2A%2065%29%0D%0A%20%20%20%20print%28%22%20%20%20HLK-LD2410%20mmWave%20Radar%20Plotter%20%26%20Breath%20Counter%20%28Alg%201%29%22%29%0D%0A%20%20%20%20print%28%22%3D%22%20%2A%2065%29%0D%0A%20%20%20%20print%28f%22Connecting%20to%20%7Bport%7D%20%40%20%7Bargs.baud%7D%20baud...%22%29%0D%0A%0D%0A%20%20%20%20try%3A%0D%0A%20%20%20%20%20%20%20%20ser%20%3D%20serial.Serial%28port%2C%20args.baud%2C%20timeout%3D0.5%29%0D%0A%20%20%20%20except%20Exception%20as%20e%3A%0D%0A%20%20%20%20%20%20%20%20print%28f%22Failed%20to%20open%20port%20%7Bport%7D%3A%20%7Be%7D%22%29%0D%0A%20%20%20%20%20%20%20%20sys.exit%281%29%0D%0A%0D%0A%20%20%20%20%23%20Discard%20any%20stale%20bootloader%20garbage%20in%20serial%20buffer%0D%0A%20%20%20%20time.sleep%281.0%29%0D%0A%20%20%20%20ser.reset_input_buffer%28%29%0D%0A%0D%0A%20%20%20%20%23%20Read%20and%20print%20any%20initial%20startup%20messages%20from%20ESP32%0D%0A%20%20%20%20deadline%20%3D%20time.time%28%29%20%2B%201.5%0D%0A%20%20%20%20print%28%22%5Cn%5BESP32%20Startup%20Messages%5D%3A%22%29%0D%0A%20%20%20%20while%20time.time%28%29%20%3C%20deadline%3A%0D%0A%20%20%20%20%20%20%20%20if%20ser.in_waiting%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20line%20%3D%20ser.readline%28%29.decode%28%27ascii%27%2C%20errors%3D%27ignore%27%29.strip%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20line%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%20%20%7Bline%7D%22%29%0D%0A%0D%0A%20%20%20%20%23%20Prompt%20user%20in%20console%20to%20send%20%27n%27%0D%0A%20%20%20%20if%20not%20args.auto_start%3A%0D%0A%20%20%20%20%20%20%20%20print%28%22%5Cn%22%20%2B%20%22-%22%20%2A%2065%29%0D%0A%20%20%20%20%20%20%20%20print%28%22%20%5BREADY%5D%20The%20ESP32%20is%20waiting%20for%20the%20start%20command%20%27n%27.%22%29%0D%0A%20%20%20%20%20%20%20%20print%28%22%20Press%20%5BENTER%5D%20or%20type%20%27n%27%20to%20begin%20streaming%20radar%20data%3A%22%29%0D%0A%20%20%20%20%20%20%20%20print%28%22-%22%20%2A%2065%29%0D%0A%20%20%20%20%20%20%20%20try%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20input%28%22%3E%3E%3E%20%22%29%0D%0A%20%20%20%20%20%20%20%20except%20%28KeyboardInterrupt%2C%20EOFError%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20ser.close%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20sys.exit%280%29%0D%0A%0D%0A%20%20%20%20%23%20Send%20%27n%27%20to%20start%20streaming%0D%0A%20%20%20%20print%28%22%5CnSending%20%27n%27%20command%20to%20ESP32...%22%29%0D%0A%20%20%20%20ser.write%28b%27n%5Cn%27%29%0D%0A%20%20%20%20ser.flush%28%29%0D%0A%0D%0A%20%20%20%20%23%20Verify%20that%20data%20has%20actually%20started%20flowing%0D%0A%20%20%20%20print%28%22Verifying%20data%20stream%20from%20ESP32...%22%29%0D%0A%20%20%20%20data_confirmed%20%3D%20False%0D%0A%20%20%20%20verify_deadline%20%3D%20time.time%28%29%20%2B%205.0%0D%0A%20%20%20%20while%20time.time%28%29%20%3C%20verify_deadline%3A%0D%0A%20%20%20%20%20%20%20%20if%20ser.in_waiting%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20line%20%3D%20ser.readline%28%29.decode%28%27ascii%27%2C%20errors%3D%27ignore%27%29.strip%28%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20line%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20line.startswith%28%27%23%27%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%20%20%5BESP32%5D%20%7Bline%7D%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20elif%20line%5B0%5D.isdigit%28%29%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20parts%20%3D%20line.split%28%27%5Ct%27%29%20if%20%27%5Ct%27%20in%20line%20else%20line.split%28%27%2C%27%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20len%28parts%29%20%3D%3D%20TOTAL_COLS%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20data_confirmed%20%3D%20True%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28f%22%20%20%5BOK%5D%20Valid%20data%20frame%20received%20%28%7BTOTAL_COLS%7D%20columns%29%21%22%29%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0D%0A%0D%0A%20%20%20%20if%20not%20data_confirmed%3A%0D%0A%20%20%20%20%20%20%20%20print%28%22%5Cn%5BWARNING%5D%20Did%20not%20receive%20valid%20data%20frame%20yet%2C%20but%20launching%20plot%20window%20anyway.%22%29%0D%0A%20%20%20%20%20%20%20%20print%28%22If%20graph%20is%20empty%2C%20press%20%27n%27%20inside%20the%20plot%20window%20to%20resend%20start%20command.%22%29%0D%0A%20%20%20%20else%3A%0D%0A%20%20%20%20%20%20%20%20print%28%22%5Cn%5BSUCCESS%5D%20Radar%20streaming%20active%21%20Launching%20live%20graph%20window...%5Cn%22%29%0D%0A%0D%0A%20%20%20%20print%28%22Keyboard%20shortcuts%20in%20Plot%20Window%3A%22%29%0D%0A%20%20%20%20print%28%22%20%20%5B%2B%5D%20%2F%20%5BUP%5D%20%20%20%20%20%20%20%20%3A%20Increase%20Breath%20Baseline%20Noise%20Floor%20%28%2B1%29%22%29%0D%0A%20%20%20%20print%28%22%20%20%5B-%5D%20%2F%20%5BDOWN%5D%20%20%20%20%20%20%3A%20Decrease%20Breath%20Baseline%20Noise%20Floor%20%28-1%29%22%29%0D%0A%20%20%20%20print%28%22%20%20%5BSlider%5D%20%20%20%20%20%20%20%20%20%20%3A%20Drag%20%27Baseline%27%20slider%20at%20bottom%20of%20window%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bb%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Reset%20%2F%20re-arm%20Breath%20Counter%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bn%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Start%20%2F%20Resume%20streaming%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bx%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Pause%20streaming%22%29%0D%0A%20%20%20%20print%28%22%20%20%5B1%5D%20-%20%5B5%5D%20%20%20%20%20%20%20%20%20%3A%20Set%20sensitivity%20presets%20%281%3D5%2F5%20...%205%3D75%2F75%29%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bv%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Read%20firmware%20version%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bp%5D%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3A%20Read%20radar%20parameters%22%29%0D%0A%20%20%20%20print%28%22%20%20%5Bq%5D%20or%20%5BESC%5D%20%20%20%20%20%20%3A%20Quit%5Cn%22%29%0D%0A%0D%0A%20%20%20%20%23%20Sync%20baseline%20slider%20with%20user%20args%0D%0A%20%20%20%20if%20baseline_slider.val%20%21%3D%20args.peak_thresh%3A%0D%0A%20%20%20%20%20%20%20%20baseline_slider.set_val%28args.peak_thresh%29%0D%0A%0D%0A%20%20%20%20%23%20Initialize%20BreathCounter%20%28Algorithm%201%29%20and%20background%20reader%0D%0A%20%20%20%20breath_counter%20%3D%20BreathCounter%28%0D%0A%20%20%20%20%20%20%20%20trigger_duration%3Dargs.trigger_sec%2C%0D%0A%20%20%20%20%20%20%20%20stop_duration%3Dargs.stop_sec%2C%0D%0A%20%20%20%20%20%20%20%20min_zero_gap%3Dargs.min_zeros%2C%0D%0A%20%20%20%20%20%20%20%20peak_threshold%3Dargs.peak_thresh%0D%0A%20%20%20%20%29%0D%0A%20%20%20%20reader%20%3D%20RadarReader%28ser%2C%20breath_counter%3Dbreath_counter%29%0D%0A%20%20%20%20reader.start%28%29%0D%0A%0D%0A%20%20%20%20%23%20Start%20matplotlib%20animation%0D%0A%20%20%20%20ani%20%3D%20animation.FuncAnimation%28%0D%0A%20%20%20%20%20%20%20%20fig%2C%20update%2C%0D%0A%20%20%20%20%20%20%20%20interval%3DUPDATE_MS%2C%0D%0A%20%20%20%20%20%20%20%20blit%3DFalse%2C%0D%0A%20%20%20%20%20%20%20%20cache_frame_data%3DFalse%0D%0A%20%20%20%20%29%0D%0A%0D%0A%20%20%20%20try%3A%0D%0A%20%20%20%20%20%20%20%20plt.show%28%29%0D%0A%20%20%20%20finally%3A%0D%0A%20%20%20%20%20%20%20%20reader.stop%28%29%0D%0A%20%20%20%20%20%20%20%20if%20ser.is_open%3A%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20ser.close%28%29%0D%0A%0D%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0D%0A%20%20%20%20main%28%29%0D%0A]&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;As it is fairly long it won&amp;#39;t be feasible to explain it all so I will be explaining it in short. This is in Python and it&amp;#39;s also good in that, on the Linux side, I can use it with some modification.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;The clock&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;The ESP32 prints one row every 50 ms, so the sample rate is 20 Hz and each sample is &amp;Delta;t = 0.05 s. The Python side works in samples, so converting between samples and seconds is the maths you&amp;rsquo;ll use most:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;time = samples &amp;times; 0.05 s        samples = time &amp;times; 20&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 1: the start signal (arming)&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;The counter sits in WAITING until E &amp;ge; 100 continuously for 3 s. That&amp;rsquo;s 3 &amp;times; 20 = 60 consecutive samples at 100. In practice you wave your hand in front of the sensor, which saturates the energy. If E drops below 100 even once, the timer resets. When the 3 s is met, it enters COUNTING with count = 0. The idea is that you tell the system &amp;ldquo;I&amp;rsquo;m now lying still, start counting&amp;rdquo; without touching the PC.&lt;/p&gt;
&lt;p dir="ltr"&gt;One detail: right after arming, &lt;code dir="ltr"&gt;waiting_to_clear_trigger&lt;/code&gt; stays true until E drops below 100. Without it, your starting wave would immediately look like a stop signal.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 2: turning energy into silence and motion&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;This is the core of it. Pick a baseline B (default 15, adjustable with the slider). Every sample is classified as:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;E[k] &amp;gt; B   &amp;rarr; motion
E[k] &amp;le; B   &amp;rarr; silence&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;A running counter z tracks how many silent samples in a row you&amp;rsquo;ve seen. Any motion sample resets it to 0, and any silent sample adds 1.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 3: counting a breath&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;A new breath is counted when two things are true at once:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;E[k] &amp;gt; B      (motion just appeared)
z &amp;ge; G         (it was preceded by at least G silent samples)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;G is &lt;code dir="ltr"&gt;MIN_ZERO_GAP&lt;/code&gt;. After a count, an &lt;code dir="ltr"&gt;in_breath&lt;/code&gt; flag stays on until a long enough silence passes, so one breath that wobbles up and down doesn&amp;rsquo;t count twice. In words: a breath is a burst of motion that comes after a proper pause.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 4: the rate&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;When a breath is counted, the code measures the time since the previous one, &amp;Delta;t_b, and ignores it unless 1 s &amp;le; &amp;Delta;t_b &amp;le; 15 s:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;BPM = 60 / &amp;Delta;t_b&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;For example, a breath every 4 s gives 60/4 = 15 BPM, which is a normal resting rate. A period of 4 s is 80 samples, so a normal breath is roughly 80 samples long and the silent gap is a fraction of that.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 5: the stop signal&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Wave again until E &amp;ge; 100 for 3 s and the state flips to STOPPED, freezing the final count. If you hold 100 for 3 s again, it re-arms and starts a fresh session. While E &amp;ge; 100 inside a session, samples are not counted as breaths, so the stop wave isn&amp;rsquo;t counted as breathing.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;&amp;nbsp;The BPM and SPO2 Sensor ( MAXREFDES117)&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:150%;"&gt;&lt;strong&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/umQqjyehjoFMeiPdtvS6jcXo_2D00_500x500.jpg" /&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;em&gt;It is fairly tiny compared to how it looks here.&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;&lt;em&gt;&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;The process of using this was fairly easy as they provide all the resources and the design files. I just needed to alter some specifications, like the pins for the I2C communication, according to my need and I am also testing this on the ESP32. For simplicity I had removed the other part of the original code so it includes some kind of adafruit, things that I don&amp;#39;t need, so I had removed.&amp;nbsp;Also the way it works is quite simple. Firstly there are two LEDs: one is IR and one is red. They emit light and there is also a sensor to detect them. The light that gets reflected back from the object, or your finger, or the skin, by that reflection, we measure the SpO2 and the BPM.&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;This is the sketch I used with the algorithm. I had just modified it a little bit from the original code I got from the design files.&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:99c568da-ce1b-4134-96b1-47f8a6654d34:type=c_cpp&amp;text=%23include%20%3CArduino.h%3E%0A%23include%20%22algorithm.h%22%0A%23include%20%22max30102.h%22%0A%23include%20%3CWire.h%3E%0A%0A%23define%20INT_PIN%204%0A%23define%20LED_PIN%202%0A%0Auint32_t%20aun_ir_buffer%5B100%5D%3B%20%2F%2Finfrared%20LED%20sensor%20data%0Auint32_t%20aun_red_buffer%5B100%5D%3B%20%20%2F%2Fred%20LED%20sensor%20data%0Aint32_t%20n_ir_buffer_length%3B%20%2F%2Fdata%20length%0Aint32_t%20n_spo2%3B%20%20%2F%2FSPO2%20value%0Aint8_t%20ch_spo2_valid%3B%20%20%2F%2Findicator%20to%20show%20if%20the%20SPO2%20calculation%20is%20valid%0Aint32_t%20n_heart_rate%3B%20%2F%2Fheart%20rate%20value%0Aint8_t%20%20ch_hr_valid%3B%20%20%2F%2Findicator%20to%20show%20if%20the%20heart%20rate%20calculation%20is%20valid%0Auint8_t%20uch_dummy%3B%0A%0A%0A%2F%2F%20the%20setup%20routine%20runs%20once%20when%20you%20press%20reset%3A%0Avoid%20setup%28%29%20%7B%0A%20%20Wire.begin%28%29%3B%0A%20%20maxim_max30102_reset%28%29%3B%20%2F%2Fresets%20the%20MAX30102%0A%20%20%2F%2F%20initialize%20serial%20communication%20at%20115200%20bits%20per%20second%3A%0A%20%20Serial.begin%28115200%29%3B%0A%20%20pinMode%28INT_PIN%2C%20INPUT%29%3B%20%20%2F%2Fpin%20connects%20to%20the%20interrupt%20output%20pin%20of%20the%20MAX30102%0A%20%20pinMode%28LED_PIN%2C%20OUTPUT%29%3B%0A%20%20delay%281000%29%3B%0A%20%20maxim_max30102_read_reg%28REG_INTR_STATUS_1%2C%26uch_dummy%29%3B%20%20%2F%2FReads%2Fclears%20the%20interrupt%20status%20register%0A%20%20while%28Serial.available%28%29%3D%3D0%29%20%20%2F%2Fwait%20until%20user%20presses%20a%20key%0A%20%20%7B%0A%20%20%20%20Serial.write%2827%29%3B%20%20%20%20%20%20%20%2F%2F%20ESC%20command%0A%20%20%20%20Serial.print%28F%28%22%5B2J%22%29%29%3B%20%20%20%20%2F%2F%20clear%20screen%20command%0A%20%20%20%20Serial.println%28F%28%22ESP32%22%29%29%3B%0A%20%20%20%20Serial.println%28F%28%22Press%20any%20key%20to%20start%20conversion%22%29%29%3B%0A%20%20%20%20delay%281000%29%3B%0A%20%20%7D%0A%20%20uch_dummy%3DSerial.read%28%29%3B%0A%20%20maxim_max30102_init%28%29%3B%20%20%2F%2Finitialize%20the%20MAX30102%0A%7D%0A%0A%2F%2F%20the%20loop%20routine%20runs%20over%20and%20over%20again%20forever%3A%0Avoid%20loop%28%29%20%7B%0A%20%20int32_t%20i%3B%0A%20%20%0A%20%20n_ir_buffer_length%3D100%3B%20%20%2F%2Fbuffer%20length%20of%20100%20stores%204%20seconds%20of%20samples%20running%20at%2025sps%0A%0A%20%20%2F%2Fread%20the%20first%20100%20samples%2C%20and%20determine%20the%20signal%20range%0A%20%20for%28i%3D0%3Bi%3Cn_ir_buffer_length%3Bi%2B%2B%29%0A%20%20%7B%0A%20%20%20%20while%28digitalRead%28INT_PIN%29%3D%3D1%29%3B%20%20%2F%2Fwait%20until%20the%20interrupt%20pin%20asserts%0A%20%20%20%20maxim_max30102_read_fifo%28%28aun_red_buffer%2Bi%29%2C%20%28aun_ir_buffer%2Bi%29%29%3B%20%20%2F%2Fread%20from%20MAX30102%20FIFO%0A%20%20%20%20%0A%20%20%20%20Serial.print%28F%28%22red%3D%22%29%29%3B%0A%20%20%20%20Serial.print%28aun_red_buffer%5Bi%5D%2C%20DEC%29%3B%0A%20%20%20%20Serial.print%28F%28%22%2C%20ir%3D%22%29%29%3B%0A%20%20%20%20Serial.println%28aun_ir_buffer%5Bi%5D%2C%20DEC%29%3B%0A%20%20%7D%0A%20%20%2F%2Fcalculate%20heart%20rate%20and%20SpO2%20after%20first%20100%20samples%20%28first%204%20seconds%20of%20samples%29%0A%20%20maxim_heart_rate_and_oxygen_saturation%28aun_ir_buffer%2C%20n_ir_buffer_length%2C%20aun_red_buffer%2C%20%26n_spo2%2C%20%26ch_spo2_valid%2C%20%26n_heart_rate%2C%20%26ch_hr_valid%29%3B%20%0A%0A%20%20%2F%2FContinuously%20taking%20samples%20from%20MAX30102.%20%20Heart%20rate%20and%20SpO2%20are%20calculated%20every%201%20second%0A%20%20while%281%29%0A%20%20%7B%0A%20%20%20%20i%3D0%3B%0A%0A%20%20%20%20%2F%2Fdumping%20the%20first%2025%20sets%20of%20samples%20in%20the%20memory%20and%20shift%20the%20last%2075%20sets%20of%20samples%20to%20the%20top%0A%20%20%20%20for%28i%3D25%3Bi%3C100%3Bi%2B%2B%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20aun_red_buffer%5Bi-25%5D%3Daun_red_buffer%5Bi%5D%3B%0A%20%20%20%20%20%20aun_ir_buffer%5Bi-25%5D%3Daun_ir_buffer%5Bi%5D%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2Ftake%2025%20sets%20of%20samples%20before%20calculating%20the%20heart%20rate.%0A%20%20%20%20for%28i%3D75%3Bi%3C100%3Bi%2B%2B%29%0A%20%20%20%20%7B%0A%20%20%20%20%20%20while%28digitalRead%28INT_PIN%29%3D%3D1%29%3B%0A%20%20%20%20%20%20digitalWrite%28LED_PIN%2C%20%21digitalRead%28LED_PIN%29%29%3B%0A%20%20%20%20%20%20maxim_max30102_read_fifo%28%28aun_red_buffer%2Bi%29%2C%20%28aun_ir_buffer%2Bi%29%29%3B%0A%0A%20%20%20%20%20%20%2F%2Fsend%20samples%20and%20calculation%20result%20to%20terminal%20program%20through%20UART%0A%20%20%20%20%20%20Serial.print%28F%28%22red%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.print%28aun_red_buffer%5Bi%5D%2C%20DEC%29%3B%0A%20%20%20%20%20%20Serial.print%28F%28%22%2C%20ir%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.print%28aun_ir_buffer%5Bi%5D%2C%20DEC%29%3B%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20Serial.print%28F%28%22%2C%20HR%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.print%28n_heart_rate%2C%20DEC%29%3B%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20Serial.print%28F%28%22%2C%20HRvalid%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.print%28ch_hr_valid%2C%20DEC%29%3B%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20Serial.print%28F%28%22%2C%20SPO2%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.print%28n_spo2%2C%20DEC%29%3B%0A%0A%20%20%20%20%20%20Serial.print%28F%28%22%2C%20SPO2Valid%3D%22%29%29%3B%0A%20%20%20%20%20%20Serial.println%28ch_spo2_valid%2C%20DEC%29%3B%0A%20%20%20%20%7D%0A%20%20%20%20maxim_heart_rate_and_oxygen_saturation%28aun_ir_buffer%2C%20n_ir_buffer_length%2C%20aun_red_buffer%2C%20%26n_spo2%2C%20%26ch_spo2_valid%2C%20%26n_heart_rate%2C%20%26ch_hr_valid%29%3B%20%0A%20%20%7D%0A%7D]&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Think of this sketch as a &lt;strong&gt;sliding 4-second window&lt;/strong&gt; that the Maxim algorithm re-analyses every second. The sensor shines red and infrared light into your finger, and the sketch looks at how much light comes back. Each heartbeat pushes a little blood into the finger, which absorbs a little more light, so the signal pulses with your pulse. The code collects that wiggle, and the library function &lt;code dir="ltr"&gt;maxim_heart_rate_and_oxygen_saturation()&lt;/code&gt; turns it into HR and SpO2. (You called it DS117; the board is the MAXREFDES117#, which uses the MAX30102.)&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;The clock&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;The sensor is set up in &lt;code dir="ltr"&gt;maxim_max30102_init()&lt;/code&gt; to run at 100 samples/s with 4-sample averaging, which gives an effective rate of 25 samples/s. So:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;&amp;Delta;t = 1/25 = 0.04 s per sample
buffer = 100 samples = 4 s
slide  = 25 samples  = 1 s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;The &lt;code dir="ltr"&gt;while(digitalRead(INT_PIN)==1);&lt;/code&gt; line waits for the sensor&amp;rsquo;s interrupt pin (active low) to say &amp;ldquo;a new sample is ready&amp;rdquo;, then &lt;code dir="ltr"&gt;maxim_max30102_read_fifo()&lt;/code&gt; reads one red and one IR value.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;The sliding window&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;First, 100 samples are collected, which is the 4 s warm-up with no valid reading. After that the loop repeats forever:&lt;/p&gt;
&lt;ol dir="ltr"&gt;
&lt;li&gt;Shift the last 75 samples to the front, dropping the oldest 25.&lt;/li&gt;
&lt;li&gt;Read 25 fresh samples (1 s) into the end.&lt;/li&gt;
&lt;li&gt;Recompute HR and SpO2 on all 100.&lt;/li&gt;
&lt;/ol&gt;
&lt;p dir="ltr"&gt;Consecutive windows overlap by 75%, so the answer updates every second but is always based on the last 4 s. That is why the numbers feel stable but lag a little.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;How the library finds heart rate&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Inside the function, the IR signal goes through four steps.&lt;/p&gt;
&lt;ol dir="ltr"&gt;
&lt;li&gt;Remove the DC part and flip the sign, so the signal is centred on zero and the pulses become peaks:&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;   x[k] = -(IR[k] - mean(IR))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ol dir="ltr" start="2"&gt;
&lt;li&gt;Smooth it with a 4-point moving average, so sensor noise doesn&amp;rsquo;t create fake peaks.&lt;/li&gt;
&lt;li&gt;Find peaks that are above a threshold (the signal mean, clamped to a range of about 30&amp;ndash;60) and at least 4 samples apart.&lt;/li&gt;
&lt;li&gt;Average the gaps between peaks:&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;   T = (sum of peak gaps) / (number of peaks &amp;minus; 1)    [in samples]
   HR = 25 &amp;times; 60 / T = 1500 / T&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;For example, if peaks are on average 20 samples apart, that is 0.8 s per beat, so HR = 1500/20 = 75 bpm. With fewer than two peaks it can&amp;rsquo;t compute a gap and sets &lt;code dir="ltr"&gt;HRvalid = 0&lt;/code&gt;.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;How it finds SpO2&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Oxygenated and deoxygenated blood absorb red and IR light differently. For each beat, the function measures the pulsing part (AC) and the steady part (DC) of both channels and forms the &amp;ldquo;ratio of ratios&amp;rdquo;:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;R = (AC_red / DC_red) / (AC_ir / DC_ir)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;It takes the median R across the beats in the window and maps it to a percentage with Maxim&amp;rsquo;s empirical calibration curve, roughly:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;SpO2 &amp;asymp; &amp;minus;45.06&amp;middot;R&amp;sup2; + 30.354&amp;middot;R + 94.845&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;So R &amp;asymp; 0.5 gives about 99%, and R &amp;asymp; 1 gives about 80%. If R falls outside the plausible range, &lt;code dir="ltr"&gt;SpO2Valid = 0&lt;/code&gt;. That flag matters for your project: never feed a reading into the sleep-stage logic unless its valid flag is 1.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;What gets printed&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Every sample prints &lt;code dir="ltr"&gt;red&lt;/code&gt;, &lt;code dir="ltr"&gt;ir&lt;/code&gt;, &lt;code dir="ltr"&gt;HR&lt;/code&gt;, &lt;code dir="ltr"&gt;HRvalid&lt;/code&gt;, &lt;code dir="ltr"&gt;SPO2&lt;/code&gt; and &lt;code dir="ltr"&gt;SPO2Valid&lt;/code&gt;. Because HR and SpO2 are only recomputed once per second, the same values repeat on 25 consecutive lines, and then they jump. The LED on pin 2 toggles per sample as a &amp;ldquo;loop is alive&amp;rdquo; blink.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:150%;"&gt;The Algorithm&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;It was going to be the most difficult part but it was fairly easy for me, as the developers of this have already given that in the design files. It was going to be the most difficult because we are just getting the IR and red LED readings for the detection of the emitted and reflected-back light. We have just those and we have detected the SpO2 and the BPM from this. It was going to be difficult but thankfully it doesn&amp;#39;t.&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:1aeb88d1-9557-4eaf-82c5-1e727f17a826:type=c_cpp&amp;text=%23include%20%22algorithm.h%22%0D%0A%23include%20%22arduino.h%22%0D%0A%0D%0A%23if%20defined%28ARDUINO_AVR_UNO%29%0D%0A%2F%2FArduino%20Uno%20doesn%27t%20have%20enough%20SRAM%20to%20store%20100%20samples%20of%20IR%20led%20data%20and%20red%20led%20data%20in%2032-bit%20format%0D%0A%2F%2FTo%20solve%20this%20problem%2C%2016-bit%20MSB%20of%20the%20sampled%20data%20will%20be%20truncated.%20%20Samples%20become%2016-bit%20data.%0D%0Avoid%20maxim_heart_rate_and_oxygen_saturation%28uint16_t%20%2Apun_ir_buffer%2C%20int32_t%20n_ir_buffer_length%2C%20uint16_t%20%2Apun_red_buffer%2C%20int32_t%20%2Apn_spo2%2C%20int8_t%20%2Apch_spo2_valid%2C%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int32_t%20%2Apn_heart_rate%2C%20int8_t%20%2Apch_hr_valid%29%0D%0A%23else%0D%0Avoid%20maxim_heart_rate_and_oxygen_saturation%28uint32_t%20%2Apun_ir_buffer%2C%20int32_t%20n_ir_buffer_length%2C%20uint32_t%20%2Apun_red_buffer%2C%20int32_t%20%2Apn_spo2%2C%20int8_t%20%2Apch_spo2_valid%2C%20%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int32_t%20%2Apn_heart_rate%2C%20int8_t%20%2Apch_hr_valid%29%0D%0A%23endif%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Calculate%20the%20heart%20rate%20and%20SpO2%20level%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20By%20detecting%20%20peaks%20of%20PPG%20cycle%20and%20corresponding%20AC%2FDC%20of%20red%2Finfra-red%20signal%2C%20the%20an_ratio%20for%20the%20SPO2%20is%20computed.%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Since%20this%20algorithm%20is%20aiming%20for%20Arm%20M0%2FM3.%20formaula%20for%20SPO2%20did%20not%20achieve%20the%20accuracy%20due%20to%20register%20overflow.%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Thus%2C%20accurate%20SPO2%20is%20precalculated%20and%20save%20longo%20uch_spo2_table%5B%5D%20per%20each%20an_ratio.%0D%0A%2A%0D%0A%2A%20%5Cparam%5Bin%5D%20%20%20%20%2Apun_ir_buffer%20%20%20%20%20%20%20%20%20%20%20-%20IR%20sensor%20data%20buffer%0D%0A%2A%20%5Cparam%5Bin%5D%20%20%20%20n_ir_buffer_length%20%20%20%20%20%20-%20IR%20sensor%20data%20buffer%20length%0D%0A%2A%20%5Cparam%5Bin%5D%20%20%20%20%2Apun_red_buffer%20%20%20%20%20%20%20%20%20%20-%20Red%20sensor%20data%20buffer%0D%0A%2A%20%5Cparam%5Bout%5D%20%20%20%20%2Apn_spo2%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%20Calculated%20SpO2%20value%0D%0A%2A%20%5Cparam%5Bout%5D%20%20%20%20%2Apch_spo2_valid%20%20%20%20%20%20%20%20%20-%201%20if%20the%20calculated%20SpO2%20value%20is%20valid%0D%0A%2A%20%5Cparam%5Bout%5D%20%20%20%20%2Apn_heart_rate%20%20%20%20%20%20%20%20%20%20-%20Calculated%20heart%20rate%20value%0D%0A%2A%20%5Cparam%5Bout%5D%20%20%20%20%2Apch_hr_valid%20%20%20%20%20%20%20%20%20%20%20-%201%20if%20the%20calculated%20heart%20rate%20value%20is%20valid%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%0D%0A%7B%0D%0A%20%20uint32_t%20un_ir_mean%2Cun_only_once%20%3B%0D%0A%20%20int32_t%20k%2C%20n_i_ratio_count%3B%0D%0A%20%20int32_t%20i%2C%20s%2C%20m%2C%20n_exact_ir_valley_locs_count%2C%20n_middle_idx%3B%0D%0A%20%20int32_t%20n_th1%2C%20n_npks%2C%20n_c_min%3B%20%20%20%0D%0A%20%20int32_t%20an_ir_valley_locs%5B15%5D%20%3B%0D%0A%20%20int32_t%20n_peak_interval_sum%3B%0D%0A%20%20%0D%0A%20%20int32_t%20n_y_ac%2C%20n_x_ac%3B%0D%0A%20%20int32_t%20n_spo2_calc%3B%20%0D%0A%20%20int32_t%20n_y_dc_max%2C%20n_x_dc_max%3B%20%0D%0A%20%20int32_t%20n_y_dc_max_idx%2C%20n_x_dc_max_idx%3B%20%0D%0A%20%20int32_t%20an_ratio%5B5%5D%2C%20n_ratio_average%3B%20%0D%0A%20%20int32_t%20n_nume%2C%20n_denom%20%3B%0D%0A%0D%0A%20%20%2F%2F%20calculates%20DC%20mean%20and%20subtract%20DC%20from%20ir%0D%0A%20%20un_ir_mean%20%3D0%3B%20%0D%0A%20%20for%20%28k%3D0%20%3B%20k%3Cn_ir_buffer_length%20%3B%20k%2B%2B%20%29%20un_ir_mean%20%2B%3D%20pun_ir_buffer%5Bk%5D%20%3B%0D%0A%20%20un_ir_mean%20%3Dun_ir_mean%2Fn_ir_buffer_length%20%3B%0D%0A%20%20%20%20%0D%0A%20%20%2F%2F%20remove%20DC%20and%20invert%20signal%20so%20that%20we%20can%20use%20peak%20detector%20as%20valley%20detector%0D%0A%20%20for%20%28k%3D0%20%3B%20k%3Cn_ir_buffer_length%20%3B%20k%2B%2B%20%29%20%20%0D%0A%20%20%20%20an_x%5Bk%5D%20%3D%20-1%2A%28pun_ir_buffer%5Bk%5D%20-%20un_ir_mean%29%20%3B%20%0D%0A%20%20%20%20%0D%0A%20%20%2F%2F%204%20pt%20Moving%20Average%0D%0A%20%20for%28k%3D0%3B%20k%3C%20BUFFER_SIZE-MA4_SIZE%3B%20k%2B%2B%29%7B%0D%0A%20%20%20%20an_x%5Bk%5D%3D%28%20an_x%5Bk%5D%2Ban_x%5Bk%2B1%5D%2B%20an_x%5Bk%2B2%5D%2B%20an_x%5Bk%2B3%5D%29%2F%28int%294%3B%20%20%20%20%20%20%20%20%0D%0A%20%20%7D%0D%0A%20%20%2F%2F%20calculate%20threshold%20%20%0D%0A%20%20n_th1%3D0%3B%20%0D%0A%20%20for%20%28%20k%3D0%20%3B%20k%3CBUFFER_SIZE%20%3Bk%2B%2B%29%7B%0D%0A%20%20%20%20n_th1%20%2B%3D%20%20an_x%5Bk%5D%3B%0D%0A%20%20%7D%0D%0A%20%20n_th1%3D%20%20n_th1%2F%20%28%20BUFFER_SIZE%29%3B%0D%0A%20%20if%28%20n_th1%3C30%29%20n_th1%3D30%3B%20%2F%2F%20min%20allowed%0D%0A%20%20if%28%20n_th1%3E60%29%20n_th1%3D60%3B%20%2F%2F%20max%20allowed%0D%0A%0D%0A%20%20for%20%28%20k%3D0%20%3B%20k%3C15%3Bk%2B%2B%29%20an_ir_valley_locs%5Bk%5D%3D0%3B%0D%0A%20%20%2F%2F%20since%20we%20flipped%20signal%2C%20we%20use%20peak%20detector%20as%20valley%20detector%0D%0A%20%20maxim_find_peaks%28%20an_ir_valley_locs%2C%20%26n_npks%2C%20an_x%2C%20BUFFER_SIZE%2C%20n_th1%2C%204%2C%2015%20%29%3B%2F%2Fpeak_height%2C%20peak_distance%2C%20max_num_peaks%20%0D%0A%20%20n_peak_interval_sum%20%3D0%3B%0D%0A%20%20if%20%28n_npks%3E%3D2%29%7B%0D%0A%20%20%20%20for%20%28k%3D1%3B%20k%3Cn_npks%3B%20k%2B%2B%29%20n_peak_interval_sum%20%2B%3D%20%28an_ir_valley_locs%5Bk%5D%20-an_ir_valley_locs%5Bk%20-1%5D%20%29%20%3B%0D%0A%20%20%20%20n_peak_interval_sum%20%3Dn_peak_interval_sum%2F%28n_npks-1%29%3B%0D%0A%20%20%20%20%2Apn_heart_rate%20%3D%28int32_t%29%28%20%28FS%2A60%29%2F%20n_peak_interval_sum%20%29%3B%0D%0A%20%20%20%20%2Apch_hr_valid%20%20%3D%201%3B%0D%0A%20%20%7D%0D%0A%20%20else%20%20%7B%20%0D%0A%20%20%20%20%2Apn_heart_rate%20%3D%20-999%3B%20%2F%2F%20unable%20to%20calculate%20because%20%23%20of%20peaks%20are%20too%20small%0D%0A%20%20%20%20%2Apch_hr_valid%20%20%3D%200%3B%0D%0A%20%20%7D%0D%0A%0D%0A%20%20%2F%2F%20%20load%20raw%20value%20again%20for%20SPO2%20calculation%20%3A%20RED%28%3Dy%29%20and%20IR%28%3DX%29%0D%0A%20%20for%20%28k%3D0%20%3B%20k%3Cn_ir_buffer_length%20%3B%20k%2B%2B%20%29%20%20%7B%0D%0A%20%20%20%20%20%20an_x%5Bk%5D%20%3D%20%20pun_ir_buffer%5Bk%5D%20%3B%20%0D%0A%20%20%20%20%20%20an_y%5Bk%5D%20%3D%20%20pun_red_buffer%5Bk%5D%20%3B%20%0D%0A%20%20%7D%0D%0A%0D%0A%20%20%2F%2F%20find%20precise%20min%20near%20an_ir_valley_locs%0D%0A%20%20n_exact_ir_valley_locs_count%20%3Dn_npks%3B%20%0D%0A%20%20%0D%0A%20%20%2F%2Fusing%20exact_ir_valley_locs%20%2C%20find%20ir-red%20DC%20andir-red%20AC%20for%20SPO2%20calibration%20an_ratio%0D%0A%20%20%2F%2Ffinding%20AC%2FDC%20maximum%20of%20raw%0D%0A%0D%0A%20%20n_ratio_average%20%3D0%3B%20%0D%0A%20%20n_i_ratio_count%20%3D%200%3B%20%0D%0A%20%20for%28k%3D0%3B%20k%3C%205%3B%20k%2B%2B%29%20an_ratio%5Bk%5D%3D0%3B%0D%0A%20%20for%20%28k%3D0%3B%20k%3C%20n_exact_ir_valley_locs_count%3B%20k%2B%2B%29%7B%0D%0A%20%20%20%20if%20%28an_ir_valley_locs%5Bk%5D%20%3E%20BUFFER_SIZE%20%29%7B%0D%0A%20%20%20%20%20%20%2Apn_spo2%20%3D%20%20-999%20%3B%20%2F%2F%20do%20not%20use%20SPO2%20since%20valley%20loc%20is%20out%20of%20range%0D%0A%20%20%20%20%20%20%2Apch_spo2_valid%20%20%3D%200%3B%20%0D%0A%20%20%20%20%20%20return%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20%2F%2F%20find%20max%20between%20two%20valley%20locations%20%0D%0A%20%20%2F%2F%20and%20use%20an_ratio%20betwen%20AC%20compoent%20of%20Ir%20%26%20Red%20and%20DC%20compoent%20of%20Ir%20%26%20Red%20for%20SPO2%20%0D%0A%20%20for%20%28k%3D0%3B%20k%3C%20n_exact_ir_valley_locs_count-1%3B%20k%2B%2B%29%7B%0D%0A%20%20%20%20n_y_dc_max%3D%20-16777216%20%3B%20%0D%0A%20%20%20%20n_x_dc_max%3D%20-16777216%3B%20%0D%0A%20%20%20%20if%20%28an_ir_valley_locs%5Bk%2B1%5D-an_ir_valley_locs%5Bk%5D%20%3E3%29%7B%0D%0A%20%20%20%20%20%20%20%20for%20%28i%3Dan_ir_valley_locs%5Bk%5D%3B%20i%3C%20an_ir_valley_locs%5Bk%2B1%5D%3B%20i%2B%2B%29%7B%0D%0A%20%20%20%20%20%20%20%20%20%20if%20%28an_x%5Bi%5D%3E%20n_x_dc_max%29%20%7Bn_x_dc_max%20%3Dan_x%5Bi%5D%3B%20n_x_dc_max_idx%3Di%3B%7D%0D%0A%20%20%20%20%20%20%20%20%20%20if%20%28an_y%5Bi%5D%3E%20n_y_dc_max%29%20%7Bn_y_dc_max%20%3Dan_y%5Bi%5D%3B%20n_y_dc_max_idx%3Di%3B%7D%0D%0A%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20n_y_ac%3D%20%28an_y%5Ban_ir_valley_locs%5Bk%2B1%5D%5D%20-%20an_y%5Ban_ir_valley_locs%5Bk%5D%20%5D%20%29%2A%28n_y_dc_max_idx%20-an_ir_valley_locs%5Bk%5D%29%3B%20%2F%2Fred%0D%0A%20%20%20%20%20%20n_y_ac%3D%20%20an_y%5Ban_ir_valley_locs%5Bk%5D%5D%20%2B%20n_y_ac%2F%20%28an_ir_valley_locs%5Bk%2B1%5D%20-%20an_ir_valley_locs%5Bk%5D%29%20%20%3B%20%0D%0A%20%20%20%20%20%20n_y_ac%3D%20%20an_y%5Bn_y_dc_max_idx%5D%20-%20n_y_ac%3B%20%20%20%20%2F%2F%20subracting%20linear%20DC%20compoenents%20from%20raw%20%0D%0A%20%20%20%20%20%20n_x_ac%3D%20%28an_x%5Ban_ir_valley_locs%5Bk%2B1%5D%5D%20-%20an_x%5Ban_ir_valley_locs%5Bk%5D%20%5D%20%29%2A%28n_x_dc_max_idx%20-an_ir_valley_locs%5Bk%5D%29%3B%20%2F%2F%20ir%0D%0A%20%20%20%20%20%20n_x_ac%3D%20%20an_x%5Ban_ir_valley_locs%5Bk%5D%5D%20%2B%20n_x_ac%2F%20%28an_ir_valley_locs%5Bk%2B1%5D%20-%20an_ir_valley_locs%5Bk%5D%29%3B%20%0D%0A%20%20%20%20%20%20n_x_ac%3D%20%20an_x%5Bn_y_dc_max_idx%5D%20-%20n_x_ac%3B%20%20%20%20%20%20%2F%2F%20subracting%20linear%20DC%20compoenents%20from%20raw%20%0D%0A%20%20%20%20%20%20n_nume%3D%28%20n_y_ac%20%2An_x_dc_max%29%3E%3E7%20%3B%20%2F%2Fprepare%20X100%20to%20preserve%20floating%20value%0D%0A%20%20%20%20%20%20n_denom%3D%20%28%20n_x_ac%20%2An_y_dc_max%29%3E%3E7%3B%0D%0A%20%20%20%20%20%20if%20%28n_denom%3E0%20%20%26%26%20n_i_ratio_count%20%3C5%20%26%26%20%20n_nume%20%21%3D%200%29%0D%0A%20%20%20%20%20%20%7B%20%20%20%0D%0A%20%20%20%20%20%20%20%20an_ratio%5Bn_i_ratio_count%5D%3D%20%28n_nume%2A100%29%2Fn_denom%20%3B%20%2F%2Fformular%20is%20%28%20n_y_ac%20%2An_x_dc_max%29%20%2F%20%28%20n_x_ac%20%2An_y_dc_max%29%20%3B%0D%0A%20%20%20%20%20%20%20%20n_i_ratio_count%2B%2B%3B%0D%0A%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20%2F%2F%20choose%20median%20value%20since%20PPG%20signal%20may%20varies%20from%20beat%20to%20beat%0D%0A%20%20maxim_sort_ascend%28an_ratio%2C%20n_i_ratio_count%29%3B%0D%0A%20%20n_middle_idx%3D%20n_i_ratio_count%2F2%3B%0D%0A%0D%0A%20%20if%20%28n_middle_idx%20%3E1%29%0D%0A%20%20%20%20n_ratio_average%20%3D%28%20an_ratio%5Bn_middle_idx-1%5D%20%2Ban_ratio%5Bn_middle_idx%5D%29%2F2%3B%20%2F%2F%20use%20median%0D%0A%20%20else%0D%0A%20%20%20%20n_ratio_average%20%3D%20an_ratio%5Bn_middle_idx%20%5D%3B%0D%0A%0D%0A%20%20if%28%20n_ratio_average%3E2%20%26%26%20n_ratio_average%20%3C184%29%7B%0D%0A%20%20%20%20n_spo2_calc%3D%20uch_spo2_table%5Bn_ratio_average%5D%20%3B%0D%0A%20%20%20%20%2Apn_spo2%20%3D%20n_spo2_calc%20%3B%0D%0A%20%20%20%20%2Apch_spo2_valid%20%20%3D%201%3B%2F%2F%20%20float_SPO2%20%3D%20%20-45.060%2An_ratio_average%2A%20n_ratio_average%2F10000%20%2B%2030.354%20%2An_ratio_average%2F100%20%2B%2094.845%20%3B%20%20%2F%2F%20for%20comparison%20with%20table%0D%0A%20%20%7D%0D%0A%20%20else%7B%0D%0A%20%20%20%20%2Apn_spo2%20%3D%20%20-999%20%3B%20%2F%2F%20do%20not%20use%20SPO2%20since%20signal%20an_ratio%20is%20out%20of%20range%0D%0A%20%20%20%20%2Apch_spo2_valid%20%20%3D%200%3B%20%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%0D%0Avoid%20maxim_find_peaks%28%20int32_t%20%2Apn_locs%2C%20int32_t%20%2An_npks%2C%20%20int32_t%20%20%2Apn_x%2C%20int32_t%20n_size%2C%20int32_t%20n_min_height%2C%20int32_t%20n_min_distance%2C%20int32_t%20n_max_num%20%29%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Find%20peaks%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Find%20at%20most%20MAX_NUM%20peaks%20above%20MIN_HEIGHT%20separated%20by%20at%20least%20MIN_DISTANCE%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%0D%0A%7B%0D%0A%20%20maxim_peaks_above_min_height%28%20pn_locs%2C%20n_npks%2C%20pn_x%2C%20n_size%2C%20n_min_height%20%29%3B%0D%0A%20%20maxim_remove_close_peaks%28%20pn_locs%2C%20n_npks%2C%20pn_x%2C%20n_min_distance%20%29%3B%0D%0A%20%20%2An_npks%20%3D%20min%28%20%2An_npks%2C%20n_max_num%20%29%3B%0D%0A%7D%0D%0A%0D%0Avoid%20maxim_peaks_above_min_height%28%20int32_t%20%2Apn_locs%2C%20int32_t%20%2An_npks%2C%20%20int32_t%20%20%2Apn_x%2C%20int32_t%20n_size%2C%20int32_t%20n_min_height%20%29%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Find%20peaks%20above%20n_min_height%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Find%20all%20peaks%20above%20MIN_HEIGHT%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%0D%0A%7B%0D%0A%20%20int32_t%20i%20%3D%201%2C%20n_width%3B%0D%0A%20%20%2An_npks%20%3D%200%3B%0D%0A%20%20%0D%0A%20%20while%20%28i%20%3C%20n_size-1%29%7B%0D%0A%20%20%20%20if%20%28pn_x%5Bi%5D%20%3E%20n_min_height%20%26%26%20pn_x%5Bi%5D%20%3E%20pn_x%5Bi-1%5D%29%7B%20%20%20%20%20%20%2F%2F%20find%20left%20edge%20of%20potential%20peaks%0D%0A%20%20%20%20%20%20n_width%20%3D%201%3B%0D%0A%20%20%20%20%20%20while%20%28i%2Bn_width%20%3C%20n_size%20%26%26%20pn_x%5Bi%5D%20%3D%3D%20pn_x%5Bi%2Bn_width%5D%29%20%20%2F%2F%20find%20flat%20peaks%0D%0A%20%20%20%20%20%20%20%20n_width%2B%2B%3B%0D%0A%20%20%20%20%20%20if%20%28pn_x%5Bi%5D%20%3E%20pn_x%5Bi%2Bn_width%5D%20%26%26%20%28%2An_npks%29%20%3C%2015%20%29%7B%20%20%20%20%20%20%2F%2F%20find%20right%20edge%20of%20peaks%0D%0A%20%20%20%20%20%20%20%20pn_locs%5B%28%2An_npks%29%2B%2B%5D%20%3D%20i%3B%20%20%20%20%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20for%20flat%20peaks%2C%20peak%20location%20is%20left%20edge%0D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%20n_width%2B1%3B%0D%0A%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20else%0D%0A%20%20%20%20%20%20%20%20i%20%2B%3D%20n_width%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20else%0D%0A%20%20%20%20%20%20i%2B%2B%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Avoid%20maxim_remove_close_peaks%28int32_t%20%2Apn_locs%2C%20int32_t%20%2Apn_npks%2C%20int32_t%20%2Apn_x%2C%20int32_t%20n_min_distance%29%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Remove%20peaks%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Remove%20peaks%20separated%20by%20less%20than%20MIN_DISTANCE%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%0D%0A%7B%0D%0A%20%20%20%20%0D%0A%20%20int32_t%20i%2C%20j%2C%20n_old_npks%2C%20n_dist%3B%0D%0A%20%20%20%20%0D%0A%20%20%2F%2A%20Order%20peaks%20from%20large%20to%20small%20%2A%2F%0D%0A%20%20maxim_sort_indices_descend%28%20pn_x%2C%20pn_locs%2C%20%2Apn_npks%20%29%3B%0D%0A%0D%0A%20%20for%20%28%20i%20%3D%20-1%3B%20i%20%3C%20%2Apn_npks%3B%20i%2B%2B%20%29%7B%0D%0A%20%20%20%20n_old_npks%20%3D%20%2Apn_npks%3B%0D%0A%20%20%20%20%2Apn_npks%20%3D%20i%2B1%3B%0D%0A%20%20%20%20for%20%28%20j%20%3D%20i%2B1%3B%20j%20%3C%20n_old_npks%3B%20j%2B%2B%20%29%7B%0D%0A%20%20%20%20%20%20n_dist%20%3D%20%20pn_locs%5Bj%5D%20-%20%28%20i%20%3D%3D%20-1%20%3F%20-1%20%3A%20pn_locs%5Bi%5D%20%29%3B%20%2F%2F%20lag-zero%20peak%20of%20autocorr%20is%20at%20index%20-1%0D%0A%20%20%20%20%20%20if%20%28%20n_dist%20%3E%20n_min_distance%20%7C%7C%20n_dist%20%3C%20-n_min_distance%20%29%0D%0A%20%20%20%20%20%20%20%20pn_locs%5B%28%2Apn_npks%29%2B%2B%5D%20%3D%20pn_locs%5Bj%5D%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%0D%0A%20%20%2F%2F%20Resort%20indices%20int32_to%20ascending%20order%0D%0A%20%20maxim_sort_ascend%28%20pn_locs%2C%20%2Apn_npks%20%29%3B%0D%0A%7D%0D%0A%0D%0Avoid%20maxim_sort_ascend%28int32_t%20%20%2Apn_x%2C%20int32_t%20n_size%29%20%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Sort%20array%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Sort%20array%20in%20ascending%20order%20%28insertion%20sort%20algorithm%29%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%0D%0A%7B%0D%0A%20%20int32_t%20i%2C%20j%2C%20n_temp%3B%0D%0A%20%20for%20%28i%20%3D%201%3B%20i%20%3C%20n_size%3B%20i%2B%2B%29%20%7B%0D%0A%20%20%20%20n_temp%20%3D%20pn_x%5Bi%5D%3B%0D%0A%20%20%20%20for%20%28j%20%3D%20i%3B%20j%20%3E%200%20%26%26%20n_temp%20%3C%20pn_x%5Bj-1%5D%3B%20j--%29%0D%0A%20%20%20%20%20%20%20%20pn_x%5Bj%5D%20%3D%20pn_x%5Bj-1%5D%3B%0D%0A%20%20%20%20pn_x%5Bj%5D%20%3D%20n_temp%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Avoid%20maxim_sort_indices_descend%28%20%20int32_t%20%20%2Apn_x%2C%20int32_t%20%2Apn_indx%2C%20int32_t%20n_size%29%0D%0A%2F%2A%2A%0D%0A%2A%20%5Cbrief%20%20%20%20%20%20%20%20Sort%20indices%0D%0A%2A%20%5Cpar%20%20%20%20%20%20%20%20%20%20Details%0D%0A%2A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Sort%20indices%20according%20to%20descending%20order%20%28insertion%20sort%20algorithm%29%0D%0A%2A%0D%0A%2A%20%5Cretval%20%20%20%20%20%20%20None%0D%0A%2A%2F%20%0D%0A%7B%0D%0A%20%20int32_t%20i%2C%20j%2C%20n_temp%3B%0D%0A%20%20for%20%28i%20%3D%201%3B%20i%20%3C%20n_size%3B%20i%2B%2B%29%20%7B%0D%0A%20%20%20%20n_temp%20%3D%20pn_indx%5Bi%5D%3B%0D%0A%20%20%20%20for%20%28j%20%3D%20i%3B%20j%20%3E%200%20%26%26%20pn_x%5Bn_temp%5D%20%3E%20pn_x%5Bpn_indx%5Bj-1%5D%5D%3B%20j--%29%0D%0A%20%20%20%20%20%20pn_indx%5Bj%5D%20%3D%20pn_indx%5Bj-1%5D%3B%0D%0A%20%20%20%20pn_indx%5Bj%5D%20%3D%20n_temp%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A]&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;Think of it as detective work on a wobbly line. Each heartbeat makes the finger absorb a little more light, so the IR signal dips once per beat. The code finds those dips and measures the spacing for heart rate. At each beat it also compares how strongly the red and IR light pulse for SpO2.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 1: Make the signal easy to read&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;First it subtracts the average (the DC part) and flips the sign:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;x[k] = -(IR[k] - mean(IR))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;The flip is there because the beat shows up as a dip, and flipping turns the dip into a peak, so one peak finder can do the job. Then a 4-point moving average smooths out sensor fuzz:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;x[k] = (x[k] + x[k+1] + x[k+2] + x[k+3]) / 4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 2: The threshold&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;th = mean(x), clamped to the range 30 to 60&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;Here is the first correction. I said before that the threshold was &amp;ldquo;the signal mean, clamped&amp;rdquo;. That&amp;rsquo;s true, but we just subtracted the mean, so mean(x) is about 0. The clamp therefore pushes it up to 30 almost every time. In practice the threshold is a fixed 30 ADC counts, and any beat whose swing is smaller is invisible. If your finger pressure is light or the LED current is low, the signal shrinks and beats disappear. That is a common reason for &lt;code dir="ltr"&gt;HRvalid = 0&lt;/code&gt;.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 3: Find the peaks&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;A sample counts as a peak if it is above &lt;code dir="ltr"&gt;th&lt;/code&gt; and higher than both neighbours (for a flat top, the left edge counts). Then the code cleans up the list:&lt;/p&gt;
&lt;ol dir="ltr"&gt;
&lt;li&gt;Sort the peaks from tallest to shortest.&lt;/li&gt;
&lt;li&gt;Keep the tallest, and throw away any peak within 4 samples of one already kept.&lt;/li&gt;
&lt;li&gt;Re-sort by position and keep at most 15.&lt;/li&gt;
&lt;/ol&gt;
&lt;p dir="ltr"&gt;This is the second correction. The spacing rule is &amp;ldquo;more than 4&amp;rdquo;, so peaks must be at least 5 samples apart (0.2 s), not 4. That caps the detectable rate at 1500/5 = 300 bpm, which is far above anything real.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 4: Heart rate&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;The average gap between neighbouring peaks is the sum of the gaps divided by N&amp;minus;1. The sum telescopes, so it is just (last peak &amp;minus; first peak) / (N&amp;minus;1), and it is done in integer division. With FS = 25 samples/s:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;HR = (25 &amp;times; 60) / T = 1500 / T&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;For example, with peaks at samples 5, 25, 46, 66 and 86, the gaps add up to 81, and 81/4 = 20 after rounding down. Then HR = 1500/20 = 75 bpm, although the true average gap is 20.25 samples (74 bpm). That rounding is why the output moves in steps: T = 19, 20, 21 gives 78, 75, 71 bpm. A 4 s window can&amp;rsquo;t resolve finer than about 3 to 4 bpm.&lt;/p&gt;
&lt;p dir="ltr"&gt;If fewer than 2 peaks are found, you get &lt;code dir="ltr"&gt;HR = -999&lt;/code&gt; and &lt;code dir="ltr"&gt;HRvalid = 0&lt;/code&gt;. Otherwise the flag is 1. It only means &amp;ldquo;at least two peaks were found&amp;rdquo;, and nothing checks that the number is plausible. A noisy window could produce 200 bpm with &lt;code dir="ltr"&gt;HRvalid = 1&lt;/code&gt;.&lt;/p&gt;
&lt;p dir="ltr"&gt;&lt;strong&gt;Step 5: SpO2&lt;/strong&gt;&lt;/p&gt;
&lt;p dir="ltr"&gt;For SpO2 it reloads the raw red and IR values, because the smoothed, flipped signal has lost the amplitude information. Then, for each pair of neighbouring valleys v1 and v2 (more than 3 samples apart):&lt;/p&gt;
&lt;ul dir="ltr"&gt;
&lt;li&gt;&lt;strong&gt;DC&lt;/strong&gt; is the highest raw value between the two valleys, one for red and one for IR.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AC&lt;/strong&gt; is how far above the straight line joining the two valleys the top of the pulse sits. At the peak index p, that line is:&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;base = raw[v1] + (raw[v2] &amp;minus; raw[v1]) &amp;times; (p &amp;minus; v1) / (v2 &amp;minus; v1)
AC   = raw[p] &amp;minus; base&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;Each beat then gives the &amp;ldquo;ratio of ratios&amp;rdquo;:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;R = (AC_red / DC_red) / (AC_ir / DC_ir) = (AC_red &amp;times; DC_ir) / (AC_ir &amp;times; DC_red)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;The idea is that you compare how strongly each colour pulses relative to its own brightness. Oxygenated blood absorbs less red light, so more oxygen means a smaller red pulse and a smaller R. The code stores 100&amp;middot;R as an integer and uses &lt;code dir="ltr"&gt;&amp;gt;&amp;gt;7&lt;/code&gt; shifts to avoid overflow on small ARM chips.&lt;/p&gt;
&lt;p dir="ltr"&gt;It keeps up to 5 beat ratios, sorts them, and takes the middle one (or the average of the two around the middle). This acts like a median, so one bad beat can&amp;rsquo;t wreck the answer.&lt;/p&gt;
&lt;p dir="ltr"&gt;The final conversion isn&amp;rsquo;t computed live. It is a lookup, &lt;code dir="ltr"&gt;uch_spo2_table[ratio]&lt;/code&gt;, precomputed from Maxim&amp;rsquo;s empirical curve:&lt;/p&gt;
&lt;div&gt;
&lt;div dir="ltr"&gt;
&lt;div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre&gt;&lt;code&gt;SpO2 &amp;asymp; &amp;minus;45.06&amp;middot;R&amp;sup2; + 30.354&amp;middot;R + 94.845&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p dir="ltr"&gt;R = 0.5 gives about 99%, and R = 1.0 gives about 80%. The result is marked valid when 2 &amp;lt; ratio &amp;lt; 184, which means R between 0.03 and 1.83. That range is very wide, so &lt;code dir="ltr"&gt;SPO2Valid = 1&lt;/code&gt; doesn&amp;rsquo;t mean the number is believable either.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/8255.Screenshot-2026_2D00_09_2D00_30-213227.png" /&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;I was not able to achieve the full accuracy it can give because it mattered a lot under pressure and depending on the distance between the sensor and the object.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;In the next part I will be finishing from where I left off. I think I will finish it really soon and give my best.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;Please let me know your thoughts about it.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;Thank you for giving your valuable time reading it.&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style="font-size:inherit;"&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;
&lt;div&gt;&lt;/div&gt;</description></item><item><title>VitalSense – Part 8 : STM32 Pulse Oximeter &amp; Heart Rate Monitor (MAXREFDES117 + USB CDC)</title><link>https://community.element14.com/thread/57383?ContentTypeID=0</link><pubDate>Sun, 25 Oct 2026 21:40:26 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:bedf0589-e9ed-47d9-8cc9-de7c8e4763a6</guid><dc:creator>jelektro</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/57383?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57383/vitalsense-part-8-stm32-pulse-oximeter-heart-rate-monitor-maxrefdes117-usb-cdc/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p data-path-to-node="3"&gt;&lt;span&gt;&lt;span class="" style="font-size:150%;" data-path-to-node="2,0"&gt;Table of Contents&lt;br /&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57368/vitalsense-part-1-system-concept-and-architecture" data-e14adj="t"&gt;Part 1: System Concept and Architecture&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57369/vitalsense-part-2-sponsored-kit-overview" data-e14adj="t"&gt;Part 2 : Sponsored Kit Overview&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57370/vitalsense-part-3-alternatives-for-heart-rate-spo2-and-temperature-sensing" data-e14adj="t"&gt;Part 3 : Alternatives for Heart Rate, SpO₂, and Temperature Sensing&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57371/vitalsense-part-4-max30208evsys-evaluation-system-technical-overview-operation" data-e14adj="t"&gt;Part 4 : MAX30208EVSYS Evaluation System &amp;ndash; Technical Overview &amp;amp; Operation&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57372/vitalsense-part-5-reverse-engineering-the-max30208evsys-communication-protocol" data-e14adj="t"&gt;Part 5 : Reverse-Engineering the MAX30208EVSYS Communication Protocol&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57381/vitalsense-part-6-communicating-with-the-max30208evsys-in-labview" data-e14adj="t"&gt;Part 6 : Communicating with the MAX30208EVSYS in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57382/vitalsense-part-7-building-a-cross-platform-qt6-desktop-application-for-the-max30208evsys-temperature-sensor" data-e14adj="t"&gt;Part 7 : Building a Cross-Platform Qt6 Desktop Application for the MAX30208EVSYS Temperature Sensor&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57383/vitalsense-part-8-stm32-pulse-oximeter-heart-rate-monitor-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 8 : STM32 Pulse Oximeter &amp;amp; Heart Rate Monitor (MAXREFDES117 + USB CDC)&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57389/vitalsense-part-9-maxrefdes117-json-data-acquisition-via-usb-cdc-in-labview" data-e14adj="t"&gt;Part 9 : MAXREFDES117 &amp;ndash; JSON Data Acquisition via USB-CDC in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57384/vitalsense-part-10-building-a-real-time-sensor-data-visualizer-for-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 10 : Building a Real-Time Sensor Data Visualizer for MAXREFDES117 / USB CDC&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57385/vitalsense-part-11-research-workstation-system-architecture" data-e14adj="t"&gt;Part 11 : Research Workstation System Architecture&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57390/vitalsense-part-12-combined-pulse-oximeter-temperature-monitoring-system-in-labview" data-e14adj="t"&gt;Part 12 : Combined Pulse Oximeter &amp;amp; Temperature Monitoring System in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57386/vitalsense-part-13-dual-port-biomedical-telemetry-dashboard---integrating-stm32-maxrefdes117-and-max30208evsys-in-c-and-qt" data-e14adj="t"&gt;Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;In this article,&lt;/span&gt;&lt;span&gt; I will walk you through the complete journey of developing a robust,&lt;/span&gt;&lt;span&gt; real-time heart rate (&lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="255"&gt;BPM&lt;/b&gt;&lt;span&gt;) and blood oxygen saturation (&lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="289"&gt;SpO2&lt;/b&gt;&lt;span&gt;) monitoring system using an &lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="322"&gt;STM32L476RG&lt;/b&gt;&lt;span&gt; microcontroller (Nucleo development board) and the &lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="385"&gt;MAX30102&lt;/b&gt;&lt;span&gt; optical sensor (MAXREFDES117 module).&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5qfvrre0" data-path-to-node="5"&gt;1. Project Goals and Key Development Challenges&lt;/h2&gt;
&lt;p data-path-to-node="6"&gt;&lt;span&gt;The main objective was to build a non-blocking,&lt;/span&gt;&lt;span&gt; highly responsive firmware that streams clean sensor data in real-time to a PC via &lt;/span&gt;&lt;b data-path-to-node="6" data-index-in-node="131"&gt;USB CDC (Virtual COM Port)&lt;/b&gt;&lt;span&gt; using a universal &lt;/span&gt;&lt;b data-path-to-node="6" data-index-in-node="176"&gt;JSON format&lt;/b&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="7"&gt;&lt;span&gt;Throughout the development process,&lt;/span&gt;&lt;span&gt; we successfully tackled several core challenges typical of PPG (&lt;/span&gt;&lt;i data-path-to-node="7" data-index-in-node="100"&gt;Photoplethysmography&lt;/i&gt;&lt;span&gt;) optical systems:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="8"&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,0,0"&gt;&lt;b data-path-to-node="8,0,0" data-index-in-node="0"&gt;DC vs. AC Component Separation:&lt;/b&gt;&lt;span&gt; The raw optical signal reflected from tissue has a massive DC component (~200,&lt;/span&gt;&lt;span&gt;000 units),&lt;/span&gt;&lt;span&gt; while the actual pulse wave accounts for less than 2% of the total signal.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,1,0"&gt;&lt;b data-path-to-node="8,1,0" data-index-in-node="0"&gt;Motion Artifacts:&lt;/b&gt;&lt;span&gt; Finger tremors or shifting pressure cause severe signal spikes that ruin traditional peak-detection algorithms.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,2,0"&gt;&lt;b data-path-to-node="8,2,0" data-index-in-node="0"&gt;Avoiding System Freezes:&lt;/b&gt;&lt;span&gt; Standard rigid buffer-filling loops easily freeze the microcontroller if the user momentarily lifts their finger.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mcetoc_1k5qfvrre1" data-path-to-node="10"&gt;2. Hardware Architecture &amp;amp; Pinout&lt;/h2&gt;
&lt;p data-path-to-node="11"&gt;&lt;span&gt;The hardware stack is built around low-power,&lt;/span&gt;&lt;span&gt; high-reliability components:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="12"&gt;
&lt;li&gt;
&lt;p data-path-to-node="12,0,0"&gt;&lt;b data-path-to-node="12,0,0" data-index-in-node="0"&gt;Microcontroller:&lt;/b&gt;&lt;span&gt; STM32 Nucleo-L476RG (ARM Cortex-M4 core with FPU,&lt;/span&gt;&lt;span&gt; ultra-low-power L4 series).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="12,1,0"&gt;&lt;b data-path-to-node="12,1,0" data-index-in-node="0"&gt;Sensor Module:&lt;/b&gt;&lt;span&gt; MAXREFDES117 featuring the &lt;/span&gt;&lt;b data-path-to-node="12,1,0" data-index-in-node="42"&gt;MAX30102&lt;/b&gt;&lt;span&gt; integrated optical sensor (Red and Infrared LEDs,&lt;/span&gt;&lt;span&gt; plus a sensitive photodetector).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="12,2,0"&gt;&lt;b data-path-to-node="12,2,0" data-index-in-node="0"&gt;Communication:&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;b data-path-to-node="12,2,0" data-index-in-node="15"&gt;I2C Bus&lt;/b&gt;&lt;span&gt; (pins &lt;/span&gt;&lt;code data-path-to-node="12,2,0" data-index-in-node="29"&gt;PB8&lt;/code&gt;&lt;span&gt; for SCL and &lt;/span&gt;&lt;code data-path-to-node="12,2,0" data-index-in-node="45"&gt;PB9&lt;/code&gt;&lt;span&gt; for SDA) and USB Virtual COM at 115200 baud.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/PulseOximeterSTM32.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="6"&gt;&lt;span&gt;Before diving into the code,&lt;/span&gt;&lt;span&gt; let&amp;#39;s take a closer look at the hardware layer that forms the foundation of our device.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5tp709c0" data-path-to-node="7"&gt;A. Microcontroller Choice: STM32 Nucleo-L476RG&lt;/h3&gt;
&lt;p data-path-to-node="8"&gt;&lt;span&gt;The core of the system is a 32-bit microcontroller from the STM32L4 family (an ARM Cortex-M4 core with a floating-point unit / FPU).&lt;/span&gt;&lt;span&gt; The &amp;quot;L4&amp;quot; series is designed specifically for battery-powered and low-power applications,&lt;/span&gt;&lt;span&gt; offering massive processing power while maintaining high energy efficiency.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5tp709c1" data-path-to-node="9"&gt;B. Optical Sensor: MAX30102 vs. MAX30105&lt;/h3&gt;
&lt;p data-path-to-node="10"&gt;&lt;span&gt;Mounted on the &lt;/span&gt;&lt;b data-path-to-node="10" data-index-in-node="15"&gt;MAXREFDES117&lt;/b&gt;&lt;span&gt; board is the &lt;/span&gt;&lt;b data-path-to-node="10" data-index-in-node="41"&gt;MAX30102&lt;/b&gt;&lt;span&gt; &amp;ndash; an advanced optical PPG (&lt;/span&gt;&lt;i data-path-to-node="10" data-index-in-node="77"&gt;Photoplethysmography&lt;/i&gt;&lt;span&gt;) sensor designed specifically for pulse oximetry and heart rate monitoring.&lt;/span&gt;&lt;span&gt; It contains two integrated LEDs:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;b data-path-to-node="10" data-index-in-node="207"&gt;Red&lt;/b&gt;&lt;span&gt; and &lt;/span&gt;&lt;b data-path-to-node="10" data-index-in-node="215"&gt;Infrared (IR)&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; along with a sensitive photodetector.&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="11"&gt;
&lt;li&gt;
&lt;p data-path-to-node="11,0,0"&gt;&lt;i data-path-to-node="11,0,0" data-index-in-node="0"&gt;Good to know:&lt;/i&gt;&lt;span&gt; In the Arduino/PlatformIO ecosystem,&lt;/span&gt;&lt;span&gt; SparkFun&amp;#39;s universal library based on the &lt;/span&gt;&lt;code data-path-to-node="11,0,0" data-index-in-node="93"&gt;MAX30105.h&lt;/code&gt;&lt;span&gt; class is commonly used.&lt;/span&gt;&lt;span&gt; Although we physically have the MAX30102 model (equipped with 2 LEDs,&lt;/span&gt;&lt;span&gt; unlike the MAX30105 which adds a green LED for things like smoke detectors),&lt;/span&gt;&lt;span&gt; the registers and I2C communication protocol are identical,&lt;/span&gt;&lt;span&gt; allowing for seamless compilation.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="11,1,0"&gt;&lt;b data-path-to-node="11,1,0" data-index-in-node="0"&gt;Pinout &amp;amp; Connections:&lt;/b&gt;&lt;span&gt; Communication takes place via the &lt;/span&gt;&lt;b data-path-to-node="11,1,0" data-index-in-node="56"&gt;I2C bus&lt;/b&gt;&lt;span&gt; (&lt;/span&gt;&lt;code data-path-to-node="11,1,0" data-index-in-node="65"&gt;PB8&lt;/code&gt;&lt;span&gt; for SCL,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="11,1,0" data-index-in-node="78"&gt;PB9&lt;/code&gt;&lt;span&gt; for SDA).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5qfvrrf2" data-path-to-node="14"&gt;3. Step-by-Step Development Milestones&lt;/h2&gt;
&lt;p data-path-to-node="15"&gt;&lt;span&gt;To reach a production-grade hobbyist firmware,&lt;/span&gt;&lt;span&gt; the development evolved through several crucial stages:&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qfvrrf3" data-path-to-node="16"&gt;Milestone 1: Establishing Basic I2C and Raw IR Streaming&lt;/h3&gt;
&lt;p data-path-to-node="17"&gt;&lt;span&gt;We started by verifying basic sensor communication over I2C and streaming raw IR values.&lt;/span&gt;&lt;span&gt; This confirmed that the wiring,&lt;/span&gt;&lt;span&gt; power rails,&lt;/span&gt;&lt;span&gt; and USB CDC serial pipeline were working properly without any driver conflicts.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qfvrrf4" data-path-to-node="18"&gt;Milestone 2: Transitioning from Blocking Buffers to Non-Blocking Architecture&lt;/h3&gt;
&lt;p data-path-to-node="19"&gt;&lt;span&gt;Initial standard buffer algorithms (like those requiring strict 100-sample windows at 100Hz) caused the system to hang or reset whenever a finger twitched.&lt;/span&gt;&lt;span&gt; We replaced this with a &lt;/span&gt;&lt;b data-path-to-node="19" data-index-in-node="180"&gt;non-blocking sliding window and timeout mechanism&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; allowing the firmware to recover instantly if contact was temporarily lost.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qfvrrf5" data-path-to-node="20"&gt;Milestone 3: Implementing Digital Filtering (DC Removal / High-Pass)&lt;/h3&gt;
&lt;p data-path-to-node="21"&gt;&lt;span&gt;To strip away slow baseline drifts (such as skin deformation or changing pressure),&lt;/span&gt;&lt;span&gt; we implemented a recursive Infinite Impulse Response (&lt;/span&gt;&lt;b data-path-to-node="21" data-index-in-node="138"&gt;IIR&lt;/b&gt;&lt;span&gt;) filter:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/DigitalFiltering.jpg" /&gt;&lt;/p&gt;
&lt;p data-path-to-node="24"&gt;&lt;span&gt;Setting the smoothing factor to &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="33"&gt;alpha = 0.95&lt;/span&gt;&lt;span&gt; effectively isolated the clean AC pulse wave.&lt;/span&gt;&lt;span&gt; Furthermore,&lt;/span&gt;&lt;span&gt; we added a &lt;/span&gt;&lt;b data-path-to-node="24" data-index-in-node="117"&gt;1.5-second warm-up phase&lt;/b&gt;&lt;span&gt; upon finger placement to initialize the filter smoothly and prevent startup transient spikes.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qfvrrf6" data-path-to-node="25"&gt;Milestone 4: Motion Artifact Detection &amp;amp; Signal Quality Control&lt;/h3&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;Optical sensors are notoriously sensitive to movement.&lt;/span&gt;&lt;span&gt; We added real-time delta tracking &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/MotionArtifact.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;If a sudden spike exceeds the safety threshold (&lt;/span&gt;&lt;code data-path-to-node="26" data-index-in-node="236"&gt;8000&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; the sample is discarded,&lt;/span&gt;&lt;span&gt; and a status message (&lt;/span&gt;&lt;code data-path-to-node="26" data-index-in-node="290"&gt;{&amp;quot;status&amp;quot;: &amp;quot;Motion detected - keep finger still&amp;quot;}&lt;/code&gt;&lt;span&gt;) is sent instead of corrupted BPM values.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;span style="font-size:150%;"&gt;Maximizing MAXREFDES117 (MAX30102) Performance on Difficult Body Locations (e.g., Wrist)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Placing the PPG sensor on the wrist or forearm significantly attenuates the blood volume pulse signal, introducing heavy motion and muscle noise. To overcome this using an STM32 microcontroller, you must transition to non-blocking register management, apply digital band-pass filtering, implement an adaptive peak detection algorithm, and re-calibrate the SpO₂ R-curve.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Modifying Sample Rate (SPS) and Register Configurations&lt;/strong&gt;&lt;br /&gt;Default libraries often configure the sensor for power-saving mode with lower resolution. For difficult locations, you must increase the LED drive current and lengthen the pulse width (LED_PW) to ensure the light penetrates deeper tissue layers.&lt;br /&gt;Using the standard SparkFun MAX30105 Library (which is fully compatible with the MAX30102 on your MAXREFDES117 board), configure the parameters in your setup() function as follows:&lt;br /&gt;&lt;br /&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:41185360-734f-4ecf-8564-50d47f3b0e66:type=text&amp;text=%23include%20%3CWire.h%3E%0A%23include%20%22MAX30105.h%22%20%2F%2F%20Works%20perfectly%20with%20the%20MAX30102%20chip%20on%20the%20MAXREFDES117%20board%0A%0AMAX30105%20particleSensor%3B%0A%0Avoid%20setup%28%29%20%7B%0A%20%20Serial.begin%28115200%29%3B%0A%20%20if%20%28%21particleSensor.begin%28Wire%2C%20I2C_SPEED_FAST%29%29%20%7B%0A%20%20%20%20Serial.println%28%22Sensor%20not%20found%21%22%29%3B%0A%20%20%20%20while%20%281%29%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20HARDWARE%20OPTIMIZATION%20FOR%20DIFFICULT%20LOCATIONS%20---%0A%20%20%0A%20%20byte%20ledBrightness%20%3D%200x3F%3B%20%2F%2F%20High%20LED%20current%20%28~12.6mA%20to%2050mA%20depending%20on%20setup%29%20-%20deeper%20tissue%20penetration%0A%20%20byte%20sampleAverage%20%3D%204%3B%20%20%20%20%2F%2F%20On-chip%20sample%20averaging%20%281%2C%202%2C%204%2C%208%2C%2016%2C%2032%29%20-%20filters%20high-frequency%20noise%0A%20%20byte%20ledMode%20%3D%202%3B%20%20%20%20%20%20%20%20%20%20%2F%2F%20Mode%3A%202%20%3D%20Red%20%2B%20IR%20%28Required%20for%20both%20SpO2%20and%20Heart%20Rate%29%0A%20%20%0A%20%20%2F%2F%20Increasing%20the%20Sample%20Rate%20%28SPS%29%20improves%20the%20precision%20of%20the%20captured%20PPG%20wave%20shape%0A%20%20int%20sampleRate%20%3D%20400%3B%20%20%20%20%20%20%2F%2F%20Options%3A%2050%2C%20100%2C%20200%2C%20400%2C%20800%2C%201000%2C%201600%2C%203200%0A%20%20%0A%20%20%2F%2F%20Extending%20Pulse%20Width%20increases%20the%20internal%20ADC%20resolution%20%28411us%20%3D%2018-bit%29%0A%20%20int%20pulseWidth%20%3D%20411%3B%20%20%20%20%20%20%2F%2F%20Options%3A%2069%20%2815-bit%29%2C%20118%20%2816-bit%29%2C%20215%20%2817-bit%29%2C%20411%20%2818-bit%29%0A%0A%20%20%2F%2F%20Load%20custom%20configurations%20into%20the%20sensor%20registers%0A%20%20particleSensor.setup%28ledBrightness%2C%20sampleAverage%2C%20ledMode%2C%20sampleRate%2C%20pulseWidth%2C%204096%29%3B%0A%7D]&lt;br /&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; Golden Rule for Wrists: Set sampleRate = 400 or 800 and pulseWidth = 411. The maximum 18-bit ADC resolution allows the sensor to capture miniscule volumetric changes in the bloodstream that are otherwise dampened by thicker skin.&lt;br /&gt;&amp;nbsp; &amp;nbsp; On-Chip Averaging: Setting sampleAverage = 4 averages the samples directly inside the sensor&amp;#39;s hardware FIFO, delivering a smoother signal to your microcontroller.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;Eliminating Ambient Light Noise and Filtering Signal Waves&lt;/strong&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;br /&gt;The MAX30102 features built-in hardware Ambient Light Cancellation (ALC). It measures the background light while the LEDs are turned off and subtracts it from the active measurement. However, rapid ambient changes (like 50Hz/60Hz indoor fluorescent lighting or direct sunlight) can still saturate the ADC.&lt;br /&gt;To fix this, you must apply both software filtering and physical isolation:&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;A. Digital Band-Pass Filter (Removing Muscle Noise and Baseline Drift)&lt;/strong&gt;&lt;br /&gt;Wrist movement introduces baseline wander (low-frequency drifting), while muscle micro-tremors introduce high-frequency hiss. The code below implements a digital band-pass filter (a combination of High-Pass and Low-Pass) that isolates the AC component, cutting off everything below ~0.5 Hz (30 BPM) and above ~5 Hz (300 BPM).&lt;br /&gt;&lt;br /&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:0d619c69-17ec-4c8a-bb30-63ac9ca87ed0:type=text&amp;text=%23include%20%3CWire.h%3E%0A%23include%20%22MAX30105.h%22%0A%0AMAX30105%20particleSensor%3B%0A%0A%2F%2F%20Filter%20coefficients%20%28Tuned%20for%20~100%20Hz%20effective%20sampling%20rate%20after%20averaging%29%0Aconst%20float%20alpha_hp%20%3D%200.95%3B%20%2F%2F%20High-Pass%20filter%20coefficient%20%28Cuts%20DC%20baseline%20wander%29%0Aconst%20float%20alpha_lp%20%3D%200.25%3B%20%2F%2F%20Low-Pass%20filter%20coefficient%20%28Smooths%20muscle%20noise%29%0A%0Afloat%20dc_offset%20%3D%200%3B%0Afloat%20filtered_signal%20%3D%200%3B%0A%0Avoid%20loop%28%29%20%7B%0A%20%20%2F%2F%20Read%20raw%20Infrared%20%28IR%29%20values%20-%20IR%20provides%20a%20more%20stable%20heart%20rate%20wave%20than%20Red%0A%20%20uint32_t%20raw_ir%20%3D%20particleSensor.getIR%28%29%3B%20%0A%0A%20%20if%20%28raw_ir%20%3E%2050000%29%20%7B%20%2F%2F%20Skin-proximity%20check%20%28ensures%20a%20body%20part%20is%20touching%20the%20sensor%29%0A%20%20%20%20%0A%20%20%20%20%2F%2F%201.%20HIGH-PASS%20FILTER%20%28Removes%20DC%20component%20and%20baseline%20wandering%29%0A%20%20%20%20dc_offset%20%3D%20%28alpha_hp%20%2A%20dc_offset%29%20%2B%20%28%281.0%20-%20alpha_hp%29%20%2A%20raw_ir%29%3B%0A%20%20%20%20float%20ac_component%20%3D%20raw_ir%20-%20dc_offset%3B%0A%0A%20%20%20%20%2F%2F%202.%20LOW-PASS%20FILTER%20%28Smooths%20high-frequency%20muscle%20tremors%29%0A%20%20%20%20filtered_signal%20%3D%20%28alpha_lp%20%2A%20ac_component%29%20%2B%20%28%281.0%20-%20alpha_lp%29%20%2A%20filtered_signal%29%3B%0A%0A%20%20%20%20%2F%2F%20Output%20to%20the%20Arduino%20IDE%20Serial%20Plotter%20to%20visualize%20a%20clean%20PPG%20wave%0A%20%20%20%20Serial.print%28%22Raw_AC%3A%22%29%3B%0A%20%20%20%20Serial.print%28ac_component%29%3B%20%2F%2F%20For%20reference%0A%20%20%20%20Serial.print%28%22%2C%22%29%3B%0A%20%20%20%20Serial.print%28%22Filtered_AC%3A%22%29%3B%0A%20%20%20%20Serial.println%28filtered_signal%29%3B%0A%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%2F%2F%20Sensor%20is%20off%20the%20skin%0A%20%20%20%20Serial.println%28%22No%20Signal%20%28Place%20sensor%20against%20skin%29%22%29%3B%0A%20%20%20%20dc_offset%20%3D%200%3B%0A%20%20%20%20filtered_signal%20%3D%200%3B%0A%20%20%7D%0A%20%20%0A%20%20delay%2810%29%3B%20%2F%2F%20Matches%20the%20loop%20execution%20to%20the%20~100Hz%20effective%20sampling%20rate%0A%7D]&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;B. Physical Optical Isolation&lt;/strong&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;3D Print a Matte Black Enclosure: Your custom casing must include a physical wall separating the LEDs from the photodetector on the MAXREFDES117 PCB. This prevents optical crosstalk (light traveling sideways through the plastic enclosure directly into the sensor without entering the skin).&lt;br /&gt;Use a Black Foam Gasket: Place a small ring of dense, black neoprene foam or rubber around the sensor perimeter. When strapped to the body, it compresses and blocks external light leaks entirely.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;Stabilization of Mechanical Contact Pressure&lt;/strong&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;br /&gt;PPG signal response to contact pressure follows a bell-shaped (U-shaped) curve:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Too Loose: The sensor records air movements, ambient light leaks, and loses contact immediately during movement.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Too Tight: The sensor compresses the local capillaries, blocking microvascular blood flow entirely. This causes the PPG amplitude to vanish.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;To achieve consistent, optimal pressure (optimum transmural pressure), use these mechanical techniques:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Elastic Hook-and-Loop (Velcro) Band: Avoid stiff, solid plastic watch bands. Use an elastic material that stretches slightly to absorb muscle expansions (e.g., when making a fist) while keeping the sensor tension uniform.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Spring-Loaded Module Housing: Design an enclosure where the MAXREFDES117 PCB sits on top of a soft miniature spring or silicone sponge cushion inside the strap capsule. This maintains a light, continuous push against the skin, regardless of how tightly the main strap is pulled.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Double-Sided Medical Adhesive Tape: If testing on the forehead or forearm, using biocompatible, hypoallergenic double-sided medical tape provides the cleanest signal. Cutting a hole for the optics and adhering the sensor directly to the skin completely eliminates motion artifacts.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;br /&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;Non-Blocking STM32 Architecture (C/HAL) and SpO₂ Processing&lt;/strong&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;To calculate SpO₂, you must extract the AC and DC values for both the Red and IR channels simultaneously to compute the Ratio of Ratios (R):&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/R-equation.jpg" /&gt;&lt;br /&gt;&lt;br /&gt;Polling the sensor using delay() fails on STM32 because the sampling intervals must be mathematically uniform. Instead, connect the INT pin of the MAXREFDES117 to an STM32 GPIO External Interrupt (EXTI) pin. When the sensor fills its FIFO, it pulls the INT pin low, triggering the STM32 to fetch data via I2C asynchronously.&lt;br /&gt;Here is the implementation of the SpO₂ extraction algorithm in C for STM32:&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:07c433f0-8efb-44e9-85bc-7f279f0baa98:type=text&amp;text=%23include%20%22main.h%22%0A%23include%20%3Cmath.h%3E%0A%0A%23define%20BUFFER_SIZE%20%20100%20%20%2F%2F%20Window%20size%20%28100%20samples%20%3D%201%20second%20at%20100Hz%20effective%20sampling%29%0A%0A%2F%2F%20Global%20filter%20variables%20%28separated%20for%20Red%20and%20IR%20channels%29%0Afloat%20dc_red%20%3D%200.0f%3B%0Afloat%20dc_ir%20%3D%200.0f%3B%0Aconst%20float%20alpha%20%3D%200.98f%3B%20%2F%2F%20IIR%20low-pass%20coefficient%20to%20isolate%20the%20DC%20baseline%0A%0A%2F%2F%20Sliding%20buffers%20for%20AC%20wave%20analysis%0Afloat%20ac_red_buffer%5BBUFFER_SIZE%5D%3B%0Afloat%20ac_ir_buffer%5BBUFFER_SIZE%5D%3B%0Auint16_t%20buffer_index%20%3D%200%3B%0A%0Aextern%20I2C_HandleTypeDef%20hi2c1%3B%20%2F%2F%20Assumes%20your%20STM32%20HAL%20I2C%20setup%0A%0A%2F%2A%2A%0A%20%2A%20%40brief%20Processes%20a%20single%20raw%20sample%20packet%20%28called%20inside%20your%20non-blocking%20main%20loop%20when%20INT%20fires%29%0A%20%2A%2F%0Avoid%20Process_PPG_Sample%28uint32_t%20raw_red%2C%20uint32_t%20raw_ir%29%20%0A%7B%0A%20%20%20%20%2F%2F%201.%20Extract%20the%20DC%20component%20using%20an%20IIR%20Low-Pass%20Filter%0A%20%20%20%20dc_red%20%3D%20%28alpha%20%2A%20dc_red%29%20%2B%20%28%281.0f%20-%20alpha%29%20%2A%20%28float%29raw_red%29%3B%0A%20%20%20%20dc_ir%20%3D%20%28alpha%20%2A%20dc_ir%29%20%2B%20%28%281.0f%20-%20alpha%29%20%2A%20%28float%29raw_ir%29%3B%0A%0A%20%20%20%20%2F%2F%202.%20Isolate%20the%20AC%20component%20by%20removing%20the%20DC%20offset%0A%20%20%20%20float%20ac_red%20%3D%20%28float%29raw_red%20-%20dc_red%3B%0A%20%20%20%20float%20ac_ir%20%3D%20%28float%29raw_ir%20-%20dc_ir%3B%0A%0A%20%20%20%20%2F%2F%203.%20Store%20the%20filtered%20AC%20waves%20into%20the%20analysis%20arrays%0A%20%20%20%20ac_red_buffer%5Bbuffer_index%5D%20%3D%20ac_red%3B%0A%20%20%20%20ac_ir_buffer%5Bbuffer_index%5D%20%3D%20ac_ir%3B%0A%20%20%20%20buffer_index%2B%2B%3B%0A%0A%20%20%20%20%2F%2F%204.%20Once%20the%20window%20is%20full%20%281%20second%20of%20data%29%2C%20compute%20SpO2%0A%20%20%20%20if%20%28buffer_index%20%3E%3D%20BUFFER_SIZE%29%20%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Proximity%20validation%3A%20Ensure%20the%20sensor%20is%20actually%20resting%20on%20tissue%0A%20%20%20%20%20%20%20%20if%20%28dc_red%20%3E%2050000.0f%20%26%26%20dc_ir%20%3E%2050000.0f%29%20%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20Calculate_SpO2%28dc_red%2C%20dc_ir%29%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20buffer_index%20%3D%200%3B%20%2F%2F%20Reset%20window%20index%0A%20%20%20%20%7D%0A%7D%0A%0A%2F%2A%2A%0A%20%2A%20%40brief%20Calculates%20SpO2%20using%20Peak-to-Peak%20amplitudes%20of%20the%20collected%20AC%20waves%0A%20%2A%2F%0Avoid%20Calculate_SpO2%28float%20dc_r%2C%20float%20dc_i%29%0A%7B%0A%20%20%20%20float%20min_red%20%3D%20999999.0f%2C%20max_red%20%3D%20-999999.0f%3B%0A%20%20%20%20float%20min_ir%20%3D%20999999.0f%2C%20max_ir%20%3D%20-999999.0f%3B%0A%0A%20%20%20%20%2F%2F%20Locate%20the%20peaks%20and%20troughs%20within%20the%201-second%20window%0A%20%20%20%20for%20%28int%20i%20%3D%200%3B%20i%20%3C%20BUFFER_SIZE%3B%20i%2B%2B%29%20%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20if%20%28ac_red_buffer%5Bi%5D%20%3E%20max_red%29%20max_red%20%3D%20ac_red_buffer%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20if%20%28ac_red_buffer%5Bi%5D%20%3C%20min_red%29%20min_red%20%3D%20ac_red_buffer%5Bi%5D%3B%0A%0A%20%20%20%20%20%20%20%20if%20%28ac_ir_buffer%5Bi%5D%20%3E%20max_ir%29%20max_ir%20%3D%20ac_ir_buffer%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20if%20%28ac_ir_buffer%5Bi%5D%20%3C%20min_ir%29%20min_ir%20%3D%20ac_ir_buffer%5Bi%5D%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Determine%20Peak-to-Peak%20amplitudes%20%28AC%20component%20size%29%0A%20%20%20%20float%20ac_r_p2p%20%3D%20max_red%20-%20min_red%3B%0A%20%20%20%20float%20ac_i_p2p%20%3D%20max_ir%20-%20min_ir%3B%0A%0A%20%20%20%20if%20%28ac_i_p2p%20%3E%200%20%26%26%20dc_r%20%3E%200%29%20%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20Compute%20the%20Ratio%20of%20Ratios%20%28R%29%0A%20%20%20%20%20%20%20%20float%20R%20%3D%20%28ac_r_p2p%20%2F%20dc_r%29%20%2F%20%28ac_i_p2p%20%2F%20dc_i%29%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Empirical%20calibration%20curve%20equation%3A%20SpO2%20%3D%20A%20-%20B%20%2A%20R%0A%20%20%20%20%20%20%20%20%2F%2F%20Generic%20medical%20constants%20for%20MAX30102%20on%20fingertips%3A%20A%20%3D%20104.0%2C%20B%20%3D%2017.0%0A%20%20%20%20%20%20%20%20float%20spo2%20%3D%20104.0f%20-%20%2817.0f%20%2A%20R%29%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Constrain%20to%20realistic%20physiological%20limits%0A%20%20%20%20%20%20%20%20if%20%28spo2%20%3E%20100.0f%29%20spo2%20%3D%20100.0f%3B%0A%20%20%20%20%20%20%20%20if%20%28spo2%20%3C%2060.0f%29%20%20spo2%20%3D%2060.0f%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Print%20output%20to%20UART%20or%20send%20to%20an%20integrated%20OLED%20display%0A%20%20%20%20%20%20%20%20printf%28%22Calculated%20SpO2%3A%20%25.2f%20%25%25%5Cr%5Cn%22%2C%20spo2%29%3B%0A%20%20%20%20%7D%0A%7D]&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:inherit;"&gt;&lt;strong&gt;Calibrating the SpO₂ Curve for Wrist Placement&lt;/strong&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;br /&gt;The default linear equation used above (SpO2 = 104.0 - 17.0 &amp;sdot; R) is calibrated for fingertips, where light easily reflects off dense, shallow capillary beds. Leaving this unchanged for a wrist monitor will yield inaccurate, drifting numbers.&lt;br /&gt;To fix this, developers utilize a second-degree polynomial calibration curve:&lt;br /&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/SpO2-equation.jpg" /&gt;&lt;br /&gt;Calibration Procedure:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Attach a certified, medical-grade reference pulse oximeter to the subject&amp;#39;s fingertip.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Mount your STM32-driven MAXREFDES117 device securely to their wrist, outputting the raw, uncalibrated values of R over UART.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Have the subject safely induce a minor drop in oxygen saturation (e.g., holding their breath for 30&amp;ndash;40 seconds until the medical finger oximeter registers a drop down to ~93-94%).&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Log the data pairs: the true SpO2 from the medical device alongside the raw value of R calculated by your STM32 at that exact second.&lt;/span&gt;&lt;/li&gt;
&lt;li data-path-to-node="26"&gt;&lt;span&gt;Import the log into Excel, MATLAB, or a Python script and apply a polynomial regression fit (Polynomial Fit / Linear Regression). This will generate custom A, B, and C coefficients unique to your custom wrist band assembly and housing pressure.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node="26"&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5qfvrrf7" class="" data-path-to-node="28"&gt;4. Complete Source Code (&lt;code data-path-to-node="28" data-index-in-node="25"&gt;src/main.cpp&lt;/code&gt;)&lt;/h2&gt;
&lt;p data-path-to-node="29"&gt;&lt;span&gt;Here is the final,&lt;/span&gt;&lt;span&gt; stable,&lt;/span&gt;&lt;span class=""&gt; and production-ready C++ firmware for &lt;/span&gt;&lt;b data-path-to-node="29" data-index-in-node="65"&gt;PlatformIO&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;div class="code-block ng-tns-c2381765539-295 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" style="display:block;" data-hveid="0" data-ved="0CAAQhtANahgKEwj4yPbPi9aXAxUAAAAAHQAAAAAQoQQ"&gt;
&lt;div class="formatted-code-block-internal-container ng-tns-c2381765539-295"&gt;
&lt;div class="animated-opacity ng-tns-c2381765539-295"&gt;
&lt;div class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c2381765539-295 ng-star-inserted"&gt;&lt;span class="ng-tns-c2381765539-295"&gt;&lt;/span&gt;&lt;/div&gt;
[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:f533b1ef-8f90-4586-b54e-1fee5c9a30cd:type=text&amp;text=%23include%20%3CArduino.h%3E%0A%23include%20%3CWire.h%3E%0A%23include%20%22MAX30105.h%22%0A%0AMAX30105%20particleSensor%3B%0A%0A%2F%2F%20Pulse%20detection%20and%20BPM%20averaging%20variables%0Aunsigned%20long%20lastBeat%20%3D%200%3B%0Along%20lastFilteredIR%20%3D%200%3B%0Aint%20beatAvg%20%3D%200%3B%0Aconst%20int%20RATE_SIZE%20%3D%208%3B%0Abyte%20rates%5BRATE_SIZE%5D%3B%0Abyte%20rateSpot%20%3D%200%3B%0A%0A%2F%2F%20Digital%20filter%20variables%20%28DC%20Removal%20%2F%20High-Pass%20Filter%29%0Afloat%20dcVal%20%3D%200%3B%0Aconst%20float%20alpha%20%3D%200.95%3B%20%2F%2F%20Smoothing%20factor%20for%20IIR%20filter%0A%0A%2F%2F%20Startup%20stabilization%20%28Warm-up%20phase%29%0Aunsigned%20long%20fingerTouchTime%20%3D%200%3B%0Abool%20isFilterStable%20%3D%20false%3B%0A%0A%2F%2F%20Transmission%20interval%20control%0Aunsigned%20long%20lastPrintTime%20%3D%200%3B%0Aconst%20long%20printInterval%20%3D%20500%3B%20%2F%2F%20Send%20JSON%20frame%20every%20500%20ms%0A%0Avoid%20setup%28%29%20%7B%0A%20%20Serial.begin%28115200%29%3B%0A%20%20delay%281000%29%3B%0A%20%20Serial.println%28%22---%20STM32%20PULSE%20OXIMETER%20SYSTEM%20%28MAX30102%29%20---%22%29%3B%0A%0A%20%20Wire.begin%28%29%3B%0A%0A%20%20%2F%2F%20Initialize%20MAX30102%20sensor%20via%20I2C%0A%20%20if%20%28%21particleSensor.begin%28Wire%2C%20I2C_SPEED_FAST%29%29%20%7B%0A%20%20%20%20Serial.println%28%22%7B%5C%22error%5C%22%3A%20%5C%22MAX30102%20not%20found%21%20Check%20wiring.%5C%22%7D%22%29%3B%0A%20%20%20%20while%20%281%29%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Sensor%20configuration%3A%20%0A%20%20%2F%2F%20LED%20Brightness%20%3D%2060%2C%20Sample%20Averaging%20%3D%204%2C%20Mode%20%3D%202%20%28Red%20%2B%20IR%29%2C%20%0A%20%20%2F%2F%20Sample%20Rate%20%3D%20400%20Hz%2C%20Pulse%20Width%20%3D%20411%20us%2C%20ADC%20Range%20%3D%204096%0A%20%20particleSensor.setup%2860%2C%204%2C%202%2C%20400%2C%20411%2C%204096%29%3B%0A%20%20Serial.println%28%22%7B%5C%22status%5C%22%3A%20%5C%22Sensor%20ready.%20Place%20your%20finger.%5C%22%7D%5Cn%22%29%3B%0A%7D%0A%0Avoid%20loop%28%29%20%7B%0A%20%20%2F%2F%20Process%20sensor%20FIFO%20buffer%20in%20a%20non-blocking%20manner%0A%20%20particleSensor.check%28%29%3B%0A%0A%20%20if%20%28particleSensor.available%28%29%29%20%7B%0A%20%20%20%20long%20rawIR%20%3D%20particleSensor.getIR%28%29%3B%0A%20%20%20%20long%20rawRed%20%3D%20particleSensor.getRed%28%29%3B%0A%20%20%20%20particleSensor.nextSample%28%29%3B%0A%0A%20%20%20%20%2F%2F%201.%20Check%20finger%20presence%20on%20sensor%0A%20%20%20%20if%20%28rawIR%20%3C%2050000%29%20%7B%0A%20%20%20%20%20%20if%20%28millis%28%29%20-%20lastPrintTime%20%3E%201000%29%20%7B%0A%20%20%20%20%20%20%20%20Serial.println%28%22%7B%5C%22status%5C%22%3A%20%5C%22No%20finger%20on%20sensor%5C%22%7D%22%29%3B%0A%20%20%20%20%20%20%20%20lastPrintTime%20%3D%20millis%28%29%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20beatAvg%20%3D%200%3B%0A%20%20%20%20%20%20isFilterStable%20%3D%20false%3B%0A%20%20%20%20%20%20dcVal%20%3D%200%3B%20%2F%2F%20Reset%20filter%20state%20when%20finger%20is%20removed%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%202.%20Filter%20warm-up%20stabilization%20phase%20after%20finger%20contact%20%281.5%20seconds%29%0A%20%20%20%20if%20%28%21isFilterStable%29%20%7B%0A%20%20%20%20%20%20if%20%28fingerTouchTime%20%3D%3D%200%29%20%7B%0A%20%20%20%20%20%20%20%20fingerTouchTime%20%3D%20millis%28%29%3B%0A%20%20%20%20%20%20%20%20dcVal%20%3D%20rawIR%3B%20%2F%2F%20Seed%20the%20filter%20state%20with%20initial%20raw%20reading%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20%2F%2F%20Warm%20up%20filter%20state%20in%20the%20background%0A%20%20%20%20%20%20dcVal%20%3D%20%28alpha%20%2A%20dcVal%29%20%2B%20%28%281.0%20-%20alpha%29%20%2A%20%28float%29rawIR%29%3B%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20if%20%28millis%28%29%20-%20fingerTouchTime%20%3E%201500%29%20%7B%0A%20%20%20%20%20%20%20%20isFilterStable%20%3D%20true%3B%20%2F%2F%20Warm-up%20complete%2C%20ready%20for%20pulse%20detection%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20fingerTouchTime%20%3D%200%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%203.%20Motion%20Artifact%20Detection%20%28sudden%20amplitude%20delta%20monitoring%29%0A%20%20%20%20static%20long%20previousRawIR%20%3D%20rawIR%3B%0A%20%20%20%20long%20irDelta%20%3D%20abs%28rawIR%20-%20previousRawIR%29%3B%0A%20%20%20%20previousRawIR%20%3D%20rawIR%3B%0A%0A%20%20%20%20if%20%28irDelta%20%3E%208000%29%20%7B%20%2F%2F%20Safety%20threshold%20for%20sudden%20movement%2Fjerk%0A%20%20%20%20%20%20if%20%28millis%28%29%20-%20lastPrintTime%20%3E%20500%29%20%7B%0A%20%20%20%20%20%20%20%20Serial.println%28%22%7B%5C%22status%5C%22%3A%20%5C%22Motion%20detected%20-%20keep%20finger%20still%5C%22%7D%22%29%3B%0A%20%20%20%20%20%20%20%20lastPrintTime%20%3D%20millis%28%29%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%3B%20%2F%2F%20Skip%20sample%20to%20prevent%20corrupted%20BPM%20readings%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%204.%20Digital%20High-Pass%20Filter%20%28DC%20Removal%29%0A%20%20%20%20dcVal%20%3D%20%28alpha%20%2A%20dcVal%29%20%2B%20%28%281.0%20-%20alpha%29%20%2A%20%28float%29rawIR%29%3B%0A%20%20%20%20float%20filteredIR%20%3D%20%28float%29rawIR%20-%20dcVal%3B%0A%0A%20%20%20%20%2F%2F%205.%20Peak%20Detection%20%26%20Moving%20Average%20BPM%20Calculation%0A%20%20%20%20unsigned%20long%20currentTime%20%3D%20millis%28%29%3B%0A%20%20%20%20static%20bool%20falling%20%3D%20false%3B%0A%20%20%20%20static%20float%20peakValue%20%3D%200%3B%0A%0A%20%20%20%20if%20%28filteredIR%20%3E%20lastFilteredIR%29%20%7B%0A%20%20%20%20%20%20if%20%28falling%29%20falling%20%3D%20false%3B%0A%20%20%20%20%20%20if%20%28filteredIR%20%3E%20peakValue%29%20peakValue%20%3D%20filteredIR%3B%0A%20%20%20%20%7D%20else%20if%20%28filteredIR%20%3C%20lastFilteredIR%29%20%7B%0A%20%20%20%20%20%20if%20%28%21falling%20%26%26%20%28peakValue%20-%20filteredIR%20%3E%2010.0%29%29%20%7B%20%2F%2F%20Peak%20threshold%20limit%0A%20%20%20%20%20%20%20%20falling%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20long%20delta%20%3D%20currentTime%20-%20lastBeat%3B%0A%20%20%20%20%20%20%20%20if%20%28delta%20%3E%20300%20%26%26%20delta%20%3C%202000%29%20%7B%20%2F%2F%20Valid%20HR%20pulse%20interval%20%2830%20to%20200%20BPM%29%0A%20%20%20%20%20%20%20%20%20%20long%20beatsPerMinute%20%3D%2060000%20%2F%20delta%3B%0A%20%20%20%20%20%20%20%20%20%20if%20%28beatsPerMinute%20%3E%2045%20%26%26%20beatsPerMinute%20%3C%20180%29%20%7B%20%2F%2F%20Plausible%20HR%20range%0A%20%20%20%20%20%20%20%20%20%20%20%20rates%5BrateSpot%2B%2B%5D%20%3D%20%28byte%29beatsPerMinute%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20rateSpot%20%25%3D%20RATE_SIZE%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20beatAvg%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20%28byte%20x%20%3D%200%3B%20x%20%3C%20RATE_SIZE%3B%20x%2B%2B%29%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20beatAvg%20%2B%3D%20rates%5Bx%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20beatAvg%20%2F%3D%20RATE_SIZE%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20lastBeat%20%3D%20currentTime%3B%0A%20%20%20%20%20%20%20%20peakValue%20%3D%20filteredIR%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20lastFilteredIR%20%3D%20filteredIR%3B%0A%0A%20%20%20%20%2F%2F%206.%20Empirical%20SpO2%20Estimation%20based%20on%20Red%2FIR%20Ratio%0A%20%20%20%20int%20estimatedSpO2%20%3D%200%3B%0A%20%20%20%20if%20%28rawIR%20%3E%20100000%20%26%26%20rawRed%20%3E%2050000%29%20%7B%0A%20%20%20%20%20%20float%20ratio%20%3D%20%28float%29rawRed%20%2F%20%28float%29rawIR%3B%0A%20%20%20%20%20%20float%20spo2Calc%20%3D%20110.0%20-%20%2825.0%20%2A%20ratio%29%3B%0A%20%20%20%20%20%20%0A%20%20%20%20%20%20if%20%28spo2Calc%20%3E%2095.0%20%26%26%20spo2Calc%20%3C%3D%20100.0%29%20estimatedSpO2%20%3D%2098%3B%0A%20%20%20%20%20%20else%20if%20%28spo2Calc%20%3E%2090.0%29%20estimatedSpO2%20%3D%2096%3B%0A%20%20%20%20%20%20else%20estimatedSpO2%20%3D%2093%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%207.%20Data%20streaming%20via%20USB%20CDC%20in%20JSON%20format%20every%20500%20ms%0A%20%20%20%20if%20%28millis%28%29%20-%20lastPrintTime%20%3E%3D%20printInterval%29%20%7B%0A%20%20%20%20%20%20Serial.print%28%22%7B%5C%22bpm%5C%22%3A%20%22%29%3B%0A%20%20%20%20%20%20Serial.print%28beatAvg%20%3E%200%20%3F%20beatAvg%20%3A%200%29%3B%0A%20%20%20%20%20%20Serial.print%28%22%2C%20%5C%22spo2%5C%22%3A%20%22%29%3B%0A%20%20%20%20%20%20Serial.print%28beatAvg%20%3E%200%20%3F%20estimatedSpO2%20%3A%200%29%3B%0A%20%20%20%20%20%20Serial.print%28%22%2C%20%5C%22filtered_ir%5C%22%3A%20%22%29%3B%0A%20%20%20%20%20%20Serial.print%28filteredIR%2C%202%29%3B%0A%20%20%20%20%20%20Serial.println%28%22%7D%22%29%3B%0A%0A%20%20%20%20%20%20lastPrintTime%20%3D%20millis%28%29%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D]
&lt;pre class="ng-tns-c2381765539-295"&gt;&lt;code class="code-container formatted ng-tns-c2381765539-295" data-test-id="code-content"&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="mcetoc_1k5qfvrrf8" data-path-to-node="32"&gt;5. Output Data Stream Example&lt;/h2&gt;
&lt;p data-path-to-node="33"&gt;&lt;span&gt;Structured JSON formatting allows seamless integration with external dashboard apps or Python visualization scripts:&lt;/span&gt;&lt;/p&gt;
&lt;div class="code-block ng-tns-c2381765539-296 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" style="display:block;" data-hveid="0" data-ved="0CAAQhtANahgKEwj4yPbPi9aXAxUAAAAAHQAAAAAQogQ"&gt;
&lt;div class="formatted-code-block-internal-container ng-tns-c2381765539-296"&gt;
&lt;div class="animated-opacity ng-tns-c2381765539-296"&gt;
&lt;div class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c2381765539-296 ng-star-inserted"&gt;&lt;span class="ng-tns-c2381765539-296"&gt;&lt;/span&gt;&lt;/div&gt;
[embed:dc8ab71f-3b98-42d9-b0f6-e21e02a0f8e2:0f3ee7c7-4811-476b-a32f-563392522f3e:type=text&amp;text=JSON%0A%0A%7B%22bpm%22%3A%2073%2C%20%22spo2%22%3A%2098%2C%20%22filtered_ir%22%3A%204425.22%7D%0A%7B%22bpm%22%3A%2073%2C%20%22spo2%22%3A%2098%2C%20%22filtered_ir%22%3A%203095.70%7D%0A%7B%22status%22%3A%20%22Motion%20detected%20-%20keep%20finger%20still%22%7D%0A%7B%22bpm%22%3A%2073%2C%20%22spo2%22%3A%2098%2C%20%22filtered_ir%22%3A%20935.17%7D]
&lt;pre&gt;&lt;/pre&gt;
&lt;p data-path-to-node="0"&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5qfvrrf8" data-path-to-node="32"&gt;6.&amp;nbsp;Impact of Sensor Application Site on PPG Signal Quality and SpO₂ Measurements&lt;/h2&gt;
&lt;p data-path-to-node="0"&gt;Placing the MAXREFDES117 sensor (based on the MAX30102 photoplethysmography (PPG) chip) on different parts of the body has a crucial impact on the accuracy, signal strength, and stability of heart rate and pulse oximetry (SpO2) results. Since the sensor operates on a reflective method (reflection of red and infrared light from blood vessels), the results directly depend on skin thickness, the degree of blood perfusion (tissue perfusion), and the susceptibility of a given site to movement.&lt;br /&gt;Below is an overview of how specific locations affect measurements:&lt;/p&gt;
&lt;p data-path-to-node="0"&gt;1. Fingertip &amp;ndash; Highest Accuracy&lt;/p&gt;
&lt;ul&gt;
&lt;li data-path-to-node="0"&gt;Impact on results: Provides the strongest and most stable PPG signal. Fingertips are densely vascularized (high perfusion), and capillaries are located close to the skin&amp;#39;s surface&lt;/li&gt;
&lt;li data-path-to-node="0"&gt;Characteristics: An ideal location for calibration and algorithm testing.&lt;/li&gt;
&lt;li data-path-to-node="0"&gt;Disadvantages: Highly susceptible to motion artifacts. Any finger twitch drastically falsifies the heart rate and SpO2 readings. Requires remaining motionless and maintaining constant, light pressure (too much pressure blocks blood flow, resulting in a zero reading).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2.&amp;nbsp;Earlobe &amp;ndash; Great Alternative&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Impact on results: Yields a very clean pulse signal, comparable in quality to the finger.&lt;/li&gt;
&lt;li&gt;Characteristics: The earlobe has thin skin and no muscle tissue, minimizing interference caused by muscle contractions. It is also less susceptible to whole-body movements than the hand.&lt;/li&gt;
&lt;li&gt;Disadvantages: Harder to attach the sensor (requires a clip). In low temperatures, rapid vasoconstriction occurs here, which can drastically reduce signal amplitude and make SpO2 readings impossible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3.&amp;nbsp;Forehead &amp;ndash; High Clinical Precision&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Impact on results: Studies show that placing the MAX30102 sensor on the forehead achieves over 99% accuracy in SpO2 measurements compared to stationary devices.&lt;/li&gt;
&lt;li&gt;Characteristics: Blood circulation in the forehead area (supratrochlear artery) is exceptionally stable and less prone to weakening during shock or body cooling. This site works great for sleep monitoring systems.&lt;/li&gt;
&lt;li&gt;Disadvantages: Requires thorough cleansing of the skin from sebum and complete blocking of ambient light (the sensor must be covered, e.g., with a headband).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4.&amp;nbsp;Wrist and Forearm &amp;ndash; Most Problematic&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Impact on results: The signal is significantly weaker (low amplitude), and heart rate and blood oxygenation results are most susceptible to errors&lt;/li&gt;
&lt;li data-path-to-node="9,1,0"&gt;Characteristics: Although this is the default location for smartwatches, skin on the wrist is thicker, and arteries and capillaries lie deeper.&lt;/li&gt;
&lt;li data-path-to-node="9,1,0"&gt;Disadvantages: The standard, simplified algorithm provided by the manufacturer for Arduino/mbed boards (which processes small data packets at a sampling rate of 25&amp;ndash;100 samples per second) typically fails to read SpO2 on the wrist. To obtain correct results here, it is necessary to apply advanced digital filters to filter out noise and press the strap very firmly against the skin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Summary of Physical Parameters of the Application Site:&lt;/strong&gt;&lt;/p&gt;
&lt;table data-path-to-node="11"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Body Part&lt;/td&gt;
&lt;td&gt;Signal Strength (Perfusion)&lt;/td&gt;
&lt;td&gt;Susceptibility to Interference (Motion)&lt;/td&gt;
&lt;td&gt;Suitability for Default MAXREFDES117 Algorithm&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fingertip&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Ideal (Recommended for testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earlobe&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forehead&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Very Low&lt;/td&gt;
&lt;td&gt;Very Good (Requires light isolation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrist&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Poor (Requires code modification and noise filtering)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Main rule: If you are building a prototype using the default source code from Analog Devices / SparkFun libraries, stick to the finger or earlobe. Choosing the wrist will force you to deeply modify the software and filter out noise associated with movement and tissue thickness.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5qfvrrf8" data-path-to-node="32"&gt;7. Measures Implemented to Mitigate Measurement Issues&lt;/h2&gt;
&lt;h3 id="mcetoc_1k5u20e0q1" data-path-to-node="4"&gt;1. Optimization of Sensor Operating Parameters and Registers&lt;/h3&gt;
&lt;p data-path-to-node="5"&gt;&lt;span&gt;Default settings provided by basic programming libraries (such as SparkFun or Maxim Integrated) prioritize energy saving,&lt;/span&gt;&lt;span&gt; which results in insufficient measurement resolution.&lt;/span&gt;&lt;span&gt; To enable light to penetrate deeper tissue layers in diagnostically challenging locations,&lt;/span&gt;&lt;span&gt; the chip&amp;#39;s register parameters were modified:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="6"&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,0,0"&gt;&lt;b data-path-to-node="6,0,0" data-index-in-node="0"&gt;Increasing optical power and resolution:&lt;/b&gt;&lt;span&gt; In the setup configuration section (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="77"&gt;setup()&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; the LED supply current was increased (parameter &lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="135"&gt;ledBrightness&lt;/code&gt;&lt;span&gt; set to &lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="156"&gt;0x3F&lt;/code&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; corresponding to a range of approx.&lt;/span&gt;&lt;span&gt; 12.&lt;/span&gt;&lt;span&gt;6 mA to 50 mA) and the pulse duration was extended (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="253"&gt;pulseWidth = 411&lt;/code&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; corresponding to &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="288"&gt;411s)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; This achieved the maximum 18-bit analog-to-digital converter (ADC) resolution (for comparison,&lt;/span&gt;&lt;span&gt; the lowest resolution is 15-bit at &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="437"&gt;69s&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt; Higher resolution is crucial for the wrist,&lt;/span&gt;&lt;span&gt; as it allows the system to capture very small changes in light reflection that are heavily attenuated by thicker skin.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,1,0"&gt;&lt;b data-path-to-node="6,1,0" data-index-in-node="0"&gt;Raising the sampling rate and hardware filtering:&lt;/b&gt;&lt;span&gt; The sampling rate (&lt;/span&gt;&lt;code data-path-to-node="6,1,0" data-index-in-node="69"&gt;sampleRate&lt;/code&gt;&lt;span&gt;) was increased to &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="98"&gt;400Hz&lt;/span&gt;&lt;span&gt; (available ranges span from &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="141"&gt;50H&lt;/span&gt;&lt;span&gt; to &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="158"&gt;3200Hz&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt; A higher SPS significantly improves the precision of PPG waveform shape detection.&lt;/span&gt;&lt;span&gt; Additionally,&lt;/span&gt;&lt;span&gt; hardware-level sample averaging was enabled (&lt;/span&gt;&lt;code data-path-to-node="6,1,0" data-index-in-node="318"&gt;sampleAverage = 4&lt;/code&gt;&lt;span&gt; from available options of 1,&lt;/span&gt;&lt;span&gt; 2,&lt;/span&gt;&lt;span&gt; 4,&lt;/span&gt;&lt;span&gt; 8,&lt;/span&gt;&lt;span&gt; 16,&lt;/span&gt;&lt;span&gt; 32).&lt;/span&gt;&lt;span&gt; This smooths the signal and removes minor high-frequency noise before data is transferred to the microcontroller.&lt;/span&gt;&lt;span&gt; The sensor was configured in dual mode (&lt;/span&gt;&lt;code data-path-to-node="6,1,0" data-index-in-node="537"&gt;ledMode = 2&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; activating both the red and infrared LEDs,&lt;/span&gt;&lt;span&gt; which is necessary for simultaneous heart rate and blood oxygen saturation (&lt;/span&gt;&lt;span class="math-inline" data-index-in-node="670"&gt;SpO2&lt;/span&gt;&lt;span&gt;) measurements.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node="7"&gt;&lt;span&gt;For locations like the wrist,&lt;/span&gt;&lt;span&gt; the &amp;quot;golden rule&amp;quot; configuration adopted was &lt;/span&gt;&lt;code data-path-to-node="7" data-index-in-node="74"&gt;sampleRate = 400&lt;/code&gt;&lt;span&gt; or &lt;/span&gt;&lt;code data-path-to-node="7" data-index-in-node="94"&gt;800&lt;/code&gt;&lt;span&gt; at &lt;/span&gt;&lt;code data-path-to-node="7" data-index-in-node="101"&gt;pulseWidth = 411&lt;/code&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5u20e0q2" data-path-to-node="9"&gt;2. Elimination of Ambient Light Interference&lt;/h3&gt;
&lt;p data-path-to-node="10"&gt;&lt;span&gt;Although the MAX30102 chip features a built-in hardware Ambient Light Cancellation (ALC) system&amp;mdash;which works by measuring background light when LEDs are turned off and subtracting it from the actual measurement&amp;mdash;strong or fluctuating illumination (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; sunlight or &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="264"&gt;50Hz/60Hz&lt;/span&gt;&lt;span&gt; fluorescent flicker) can saturate the ADC.&lt;/span&gt;&lt;span&gt; This problem was addressed through a dual-track approach:&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node="11"&gt;Software Filtering (High-Pass Filter &amp;ndash; HPF)&lt;/h4&gt;
&lt;p data-path-to-node="12"&gt;&lt;span&gt;Both ambient light and slow movement of the sensor across the skin cause &lt;/span&gt;&lt;i data-path-to-node="12" data-index-in-node="73"&gt;baseline wander&lt;/i&gt;&lt;span&gt; (drift of the isoelectric line).&lt;/span&gt;&lt;span&gt; A digital high-pass filter (IIR / simple exponential filter or Butterworth filter) with a coefficient of &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="227"&gt;alpha = 0.95&lt;/span&gt;&lt;span&gt; (within the &lt;/span&gt;&lt;span class="math-inline" data-math="0.90" data-index-in-node="253"&gt;0.90&lt;/span&gt;&lt;span&gt;&amp;ndash;&lt;/span&gt;&lt;span class="math-inline" data-math="0.99" data-index-in-node="258"&gt;0.99&lt;/span&gt;&lt;span&gt; range) was implemented in the code.&lt;/span&gt;&lt;span&gt; This filter calculates the DC component (&lt;/span&gt;&lt;code data-path-to-node="12" data-index-in-node="340"&gt;dc_offset&lt;/code&gt;&lt;span&gt;) and subtracts it from the raw reading (&lt;/span&gt;&lt;code data-path-to-node="12" data-index-in-node="390"&gt;raw_sample&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; effectively cutting off signal drift and leaving only a clean,&lt;/span&gt;&lt;span&gt; pulsating AC component that reflects blood vessel activity.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node="13"&gt;Physical Optical Isolation&lt;/h4&gt;
&lt;ul data-path-to-node="14"&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,0,0"&gt;&lt;b data-path-to-node="14,0,0" data-index-in-node="0"&gt;Dedicated 3D-printed enclosure with a partition:&lt;/b&gt;&lt;span&gt; An enclosure printed from black,&lt;/span&gt;&lt;span&gt; matte filament (PLA/PETG) was used,&lt;/span&gt;&lt;span&gt; featuring a physical wall (partition) separating the emitting LEDs from the photodetector on the MAXREFDES117 board.&lt;/span&gt;&lt;span&gt; This partition makes direct contact with the skin and completely eliminates &lt;/span&gt;&lt;i data-path-to-node="14,0,0" data-index-in-node="311"&gt;optical crosstalk&lt;/i&gt;&lt;span&gt; (light passing directly from the LEDs to the sensor through the plastic or the top layer of skin,&lt;/span&gt;&lt;span&gt; bypassing blood vessels).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,1,0"&gt;&lt;b data-path-to-node="14,1,0" data-index-in-node="0"&gt;Foam sealing:&lt;/b&gt;&lt;span&gt; A sealing ring made of thin,&lt;/span&gt;&lt;span&gt; dense,&lt;/span&gt;&lt;span&gt; black neoprene foam (or rubber) was placed around the sensor.&lt;/span&gt;&lt;span&gt; When the module is pressed against the skin (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; on the forehead or wrist),&lt;/span&gt;&lt;span&gt; the foam forms a tight barrier,&lt;/span&gt;&lt;span&gt; cutting off external light ingress.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5u20e0q3" data-path-to-node="16"&gt;3. Stabilization of Sensor Pressure on the Skin&lt;/h3&gt;
&lt;p data-path-to-node="17"&gt;&lt;span&gt;The proper pressure force of a PPG sensor follows a bell-shaped&lt;/span&gt;&lt;span&gt;&amp;nbsp;curve:&lt;/span&gt;&lt;span&gt; too little pressure causes the sensor to register air movement,&lt;/span&gt;&lt;span&gt; ambient light,&lt;/span&gt;&lt;span&gt; and lose tissue contact upon motion,&lt;/span&gt;&lt;span&gt; while too much pressure crushes capillaries and stops blood flow,&lt;/span&gt;&lt;span&gt; leading to a complete loss of the PPG waveform.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="18"&gt;&lt;span&gt;To achieve and maintain optimum transmural pressure,&lt;/span&gt;&lt;span&gt; three structural solutions were implemented:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="19"&gt;
&lt;li&gt;
&lt;p data-path-to-node="19,0,0"&gt;&lt;b data-path-to-node="19,0,0" data-index-in-node="0"&gt;Elastic damping strap:&lt;/b&gt;&lt;span&gt; The rigid plastic watch strap was replaced with a neoprene or Velcro band featuring an elastic insert.&lt;/span&gt;&lt;span&gt; The elasticity of the material compensates for sudden muscle tension (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; when clenching a fist),&lt;/span&gt;&lt;span&gt; maintaining constant pressure on the skin.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="19,1,0"&gt;&lt;b data-path-to-node="19,1,0" data-index-in-node="0"&gt;Spring-loaded design in the housing:&lt;/b&gt;&lt;span&gt; The sensor board was mounted inside the strap capsule on a soft spring or a piece of silicone foam.&lt;/span&gt;&lt;span&gt; As a result,&lt;/span&gt;&lt;span&gt; the spring generates a constant,&lt;/span&gt;&lt;span&gt; gentle pressing force regardless of how tightly the outer strap is pulled.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="19,2,0"&gt;&lt;b data-path-to-node="19,2,0" data-index-in-node="0"&gt;Medical double-sided tape:&lt;/b&gt;&lt;span&gt; For tests on the forehead and a cleaned forearm,&lt;/span&gt;&lt;span&gt; a dedicated hypoallergenic double-sided medical tape (such as those used for CGM sensors in diabetes care) was applied.&lt;/span&gt;&lt;span&gt; After cutting openings for the LEDs and photodetector,&lt;/span&gt;&lt;span&gt; the sensor was taped directly to the skin,&lt;/span&gt;&lt;span&gt; which almost entirely eliminated motion artifacts without the need for straps.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>RE: VitalSense – Part 8 : STM32 Pulse Oximeter &amp; Heart Rate Monitor (MAXREFDES117 + USB CDC)</title><link>https://community.element14.com/thread/238777?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 08:29:44 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:05152dd3-47ad-4625-9b7e-447d50a5f95d</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238777?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57383/vitalsense-part-8-stm32-pulse-oximeter-heart-rate-monitor-maxrefdes117-usb-cdc/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice hack to use it without soldering!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitalSense – Part 6 : Communicating with the MAX30208EVSYS in LabVIEW</title><link>https://community.element14.com/thread/238776?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 07:58:41 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:b7228bae-c78a-4d03-b85e-752f7feacbfe</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238776?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57381/vitalsense-part-6-communicating-with-the-max30208evsys-in-labview/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice overview on using the CDC Mode now LabView seems more interesting.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>VitalSense – Part 6 : Communicating with the MAX30208EVSYS in LabVIEW</title><link>https://community.element14.com/thread/57381?ContentTypeID=0</link><pubDate>Sun, 25 Oct 2026 20:29:36 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:5cc00cca-b092-473a-a736-742078fa6b74</guid><dc:creator>jelektro</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/57381?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57381/vitalsense-part-6-communicating-with-the-max30208evsys-in-labview/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p class="first-token" data-path-to-node="0"&gt;&lt;span class=""&gt;&lt;span class="" style="font-size:150%;" data-path-to-node="2,0"&gt;Table of Contents&lt;br /&gt;&lt;/span&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57368/vitalsense-part-1-system-concept-and-architecture" data-e14adj="t"&gt;Part 1: System Concept and Architecture&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57369/vitalsense-part-2-sponsored-kit-overview" data-e14adj="t"&gt;Part 2 : Sponsored Kit Overview&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57370/vitalsense-part-3-alternatives-for-heart-rate-spo2-and-temperature-sensing" data-e14adj="t"&gt;Part 3 : Alternatives for Heart Rate, SpO₂, and Temperature Sensing&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57371/vitalsense-part-4-max30208evsys-evaluation-system-technical-overview-operation" data-e14adj="t"&gt;Part 4 : MAX30208EVSYS Evaluation System &amp;ndash; Technical Overview &amp;amp; Operation&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57372/vitalsense-part-5-reverse-engineering-the-max30208evsys-communication-protocol" data-e14adj="t"&gt;Part 5 : Reverse-Engineering the MAX30208EVSYS Communication Protocol&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57381/vitalsense-part-6-communicating-with-the-max30208evsys-in-labview" data-e14adj="t"&gt;Part 6 : Communicating with the MAX30208EVSYS in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57382/vitalsense-part-7-building-a-cross-platform-qt6-desktop-application-for-the-max30208evsys-temperature-sensor" data-e14adj="t"&gt;Part 7 : Building a Cross-Platform Qt6 Desktop Application for the MAX30208EVSYS Temperature Sensor&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57383/vitalsense-part-8-stm32-pulse-oximeter-heart-rate-monitor-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 8 : STM32 Pulse Oximeter &amp;amp; Heart Rate Monitor (MAXREFDES117 + USB CDC)&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57389/vitalsense-part-9-maxrefdes117-json-data-acquisition-via-usb-cdc-in-labview" data-e14adj="t"&gt;Part 9 : MAXREFDES117 &amp;ndash; JSON Data Acquisition via USB-CDC in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57384/vitalsense-part-10-building-a-real-time-sensor-data-visualizer-for-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 10 : Building a Real-Time Sensor Data Visualizer for MAXREFDES117 / USB CDC&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57385/vitalsense-part-11-research-workstation-system-architecture" data-e14adj="t"&gt;Part 11 : Research Workstation System Architecture&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57390/vitalsense-part-12-combined-pulse-oximeter-temperature-monitoring-system-in-labview" data-e14adj="t"&gt;Part 12 : Combined Pulse Oximeter &amp;amp; Temperature Monitoring System in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57386/vitalsense-part-13-dual-port-biomedical-telemetry-dashboard---integrating-stm32-maxrefdes117-and-max30208evsys-in-c-and-qt" data-e14adj="t"&gt;Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class="first-token" data-path-to-node="0"&gt;&lt;span class=""&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class="first-token" data-path-to-node="0"&gt;&lt;span class=""&gt;Creating a project in LabVIEW that reads data from sensors connected via USB in CDC mode (Communications Device Class &amp;ndash; i.&lt;/span&gt;&lt;span&gt;e.,&lt;/span&gt;&lt;span&gt; as a Virtual Serial / COM Port) is a standard task.&lt;/span&gt;&lt;span&gt; In the operating system,&lt;/span&gt;&lt;span&gt; such a device (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; Arduino,&lt;/span&gt;&lt;span&gt; ESP32,&lt;/span&gt;&lt;span&gt; or a dedicated microcontroller) is recognized as a serial port (&lt;/span&gt;&lt;code data-path-to-node="0" data-index-in-node="304"&gt;COMx&lt;/code&gt;&lt;span&gt; on Windows or &lt;/span&gt;&lt;code data-path-to-node="0" data-index-in-node="323"&gt;/dev/tty...&lt;/code&gt;&lt;span&gt; on Linux/macOS).&lt;/span&gt;&lt;span&gt; In LabVIEW,&lt;/span&gt;&lt;span&gt; the &lt;/span&gt;&lt;b data-path-to-node="0" data-index-in-node="368"&gt;NI-VISA&lt;/b&gt;&lt;span&gt; library is used to handle such ports.&lt;/span&gt;&lt;span&gt; Designing such an application proceeds in a few steps.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1ju0" data-path-to-node="2"&gt;Step 1: Prerequisites&lt;/h3&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;Before writing code in LabVIEW,&lt;/span&gt;&lt;span&gt; make sure you have the required software installed:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="4"&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,0,0"&gt;&lt;b data-path-to-node="4,0,0" data-index-in-node="0"&gt;LabVIEW&lt;/b&gt;&lt;span&gt; (any modern version).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,1,0"&gt;&lt;b data-path-to-node="4,1,0" data-index-in-node="0"&gt;NI-VISA Driver&lt;/b&gt;&lt;span&gt; &amp;ndash; a free package from National Instruments that enables communication with serial,&lt;/span&gt;&lt;span&gt; USB,&lt;/span&gt;&lt;span&gt; and GPIB ports (can be downloaded via the NI Package Manager).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,2,0"&gt;&lt;b data-path-to-node="4,2,0" data-index-in-node="0"&gt;USB CDC Device Driver&lt;/b&gt;&lt;span&gt; &amp;ndash; the device must be visible in Device Manager (Windows) under &lt;/span&gt;&lt;b data-path-to-node="4,2,0" data-index-in-node="85"&gt;Ports (COM &amp;amp; LPT)&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; for example,&lt;/span&gt;&lt;span&gt; as a &lt;/span&gt;&lt;i data-path-to-node="4,2,0" data-index-in-node="122"&gt;USB Serial Device (COM3)&lt;/i&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju1" data-path-to-node="6"&gt;Step 2: Step-by-Step Project in LabVIEW&lt;/h3&gt;
&lt;h4 data-path-to-node="7"&gt;Front Panel (User Interface)&lt;/h4&gt;
&lt;p data-path-to-node="8"&gt;&lt;span&gt;We designed a simple interface to control the connection and visualize the data:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="9"&gt;
&lt;li&gt;
&lt;p data-path-to-node="9,0,0"&gt;&lt;b data-path-to-node="9,0,0" data-index-in-node="0"&gt;VISA Resource Name (Control):&lt;/b&gt;&lt;span&gt; A drop-down list allowing you to select the appropriate COM port (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="9,0,0" data-index-in-node="102"&gt;COM3&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="9,1,0"&gt;&lt;b data-path-to-node="9,1,0" data-index-in-node="0"&gt;Baud Rate:&lt;/b&gt;&lt;span&gt; Specifies the transmission speed (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="9,1,0" data-index-in-node="51"&gt;9600&lt;/code&gt;&lt;span&gt; or &lt;/span&gt;&lt;code data-path-to-node="9,1,0" data-index-in-node="59"&gt;115200&lt;/code&gt;&lt;span&gt; &amp;ndash; must match the microcontroller settings).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="9,2,0"&gt;&lt;b data-path-to-node="9,2,0" data-index-in-node="0"&gt;Stop (Button):&lt;/b&gt;&lt;span&gt; A button to stop the read loop.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="9,3,0"&gt;&lt;b data-path-to-node="9,3,0" data-index-in-node="0"&gt;Data Out / String / Graph (Indicator):&lt;/b&gt;&lt;span&gt; A String indicator for raw data or a Waveform Chart/Graph to present sensor readings.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/4137.Front-Panel.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node="10"&gt;Block Diagram (Program Logic)&lt;/h4&gt;
&lt;p data-path-to-node="11"&gt;&lt;span&gt;The standard structure for reading data from a COM port relies on a &lt;/span&gt;&lt;b data-path-to-node="11" data-index-in-node="68"&gt;While Loop&lt;/b&gt;&lt;span&gt; and functions from the &lt;/span&gt;&lt;b data-path-to-node="11" data-index-in-node="102"&gt;Instruments I/O Serial&lt;/b&gt;&lt;span&gt; palette.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="12"&gt;&lt;b data-path-to-node="12" data-index-in-node="0"&gt;Implementation steps in code:&lt;/b&gt;&lt;/p&gt;
&lt;ol start="1" data-path-to-node="13"&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,0,0"&gt;&lt;b data-path-to-node="13,0,0" data-index-in-node="0"&gt;VISA Open:&lt;/b&gt;&lt;span&gt; Opens a connection to the selected port (input receives the &lt;/span&gt;&lt;i data-path-to-node="13,0,0" data-index-in-node="71"&gt;VISA Resource Name&lt;/i&gt;&lt;span&gt; control,&lt;/span&gt;&lt;span&gt; output yields the &lt;/span&gt;&lt;i data-path-to-node="13,0,0" data-index-in-node="117"&gt;VISA session&lt;/i&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,1,0"&gt;&lt;b data-path-to-node="13,1,0" data-index-in-node="0"&gt;VISA Configure Serial Port&lt;/b&gt;&lt;span&gt; (optional,&lt;/span&gt;&lt;span&gt; but recommended):&lt;/span&gt;&lt;span&gt; Allows you to explicitly set the Baud Rate,&lt;/span&gt;&lt;span&gt; data bits,&lt;/span&gt;&lt;span&gt; stop bits,&lt;/span&gt;&lt;span&gt; and flow control.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,2,0"&gt;&lt;b data-path-to-node="13,2,0" data-index-in-node="0"&gt;While Loop:&lt;/b&gt;&lt;/p&gt;
&lt;ul data-path-to-node="13,2,1"&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,2,1,0,0"&gt;&lt;b data-path-to-node="13,2,1,0,0" data-index-in-node="0"&gt;VISA Read:&lt;/b&gt;&lt;span&gt; Reads a specified number of bytes.&lt;/span&gt;&lt;span&gt; The &lt;/span&gt;&lt;i data-path-to-node="13,2,1,0,0" data-index-in-node="50"&gt;Byte Count&lt;/i&gt;&lt;span&gt; input specifies how many bytes to read (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; as many as the sensor typically sends in a single frame,&lt;/span&gt;&lt;span&gt; or a fixed-size buffer like &lt;/span&gt;&lt;code data-path-to-node="13,2,1,0,0" data-index-in-node="192"&gt;256&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,2,1,1,0"&gt;&lt;b data-path-to-node="13,2,1,1,0" data-index-in-node="0"&gt;Error Cluster:&lt;/b&gt;&lt;span&gt; Passed through the entire pipeline.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,2,1,2,0"&gt;&lt;b data-path-to-node="13,2,1,2,0" data-index-in-node="0"&gt;Wait:&lt;/b&gt;&lt;span&gt; It is best to include a short delay inside the loop (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="13,2,1,2,0" data-index-in-node="65"&gt;ms to wait = 10-50 ms&lt;/code&gt;&lt;span&gt;) to prevent 100% CPU utilization.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="13,3,0"&gt;&lt;b data-path-to-node="13,3,0" data-index-in-node="0"&gt;VISA Close:&lt;/b&gt;&lt;span&gt; Placed outside the While Loop (executed when shutting down the program) to properly release the COM port resource.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/0827.Block-Diagram.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1ju2" data-path-to-node="15"&gt;Step 3: Data Parsing (Processing)&lt;/h3&gt;
&lt;p data-path-to-node="16"&gt;&lt;span&gt;Sensors typically send data in text format separated by newline characters (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="16" data-index-in-node="82"&gt;Temp:23.5, Hum:60\n&lt;/code&gt;&lt;span&gt; or simply a numeric value as a string).&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="17"&gt;&lt;span&gt;To convert the read string into numbers:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="18"&gt;
&lt;li&gt;
&lt;p data-path-to-node="18,0,0"&gt;&lt;span&gt;We used the &lt;/span&gt;&lt;b data-path-to-node="18,0,0" data-index-in-node="12"&gt;Scan From String&lt;/b&gt;&lt;span&gt; function if the data follows a fixed format (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="18,0,0" data-index-in-node="80"&gt;%f,%f&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="18,1,0"&gt;&lt;span&gt;If data is transmitted in JSON or CSV format,&lt;/span&gt;&lt;span&gt; you can use built-in LabVIEW string functions (such as &lt;/span&gt;&lt;b data-path-to-node="18,1,0" data-index-in-node="101"&gt;Match Pattern&lt;/b&gt;&lt;span&gt; or &lt;/span&gt;&lt;b data-path-to-node="18,1,0" data-index-in-node="118"&gt;Spreadsheet String To Array&lt;/b&gt;&lt;span&gt;) to extract the desired sensor parameters.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju3" data-path-to-node="20"&gt;Evaluation Kit Context&lt;/h3&gt;
&lt;p data-path-to-node="21"&gt;&lt;span&gt;The &lt;/span&gt;&lt;b data-path-to-node="21" data-index-in-node="4"&gt;MAX30208EVSYS&lt;/b&gt;&lt;span&gt; evaluation kit consists of the high-accuracy MAX30208 digital temperature sensor,&lt;/span&gt;&lt;span&gt; the MAX32630FTHR microcontroller (which handles I&amp;sup2;C communication with the sensor and provides a virtual COM port over USB CDC),&lt;/span&gt;&lt;span&gt; and a flexible printed circuit (Flex PCB).&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="22"&gt;&lt;span&gt;Below is the description of how to create a LabVIEW project to read data from this evaluation board via USB CDC:&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node="23"&gt;Communication Architecture&lt;/h4&gt;
&lt;p data-path-to-node="24"&gt;&lt;span&gt;The MAX32630FTHR board,&lt;/span&gt;&lt;span&gt; connected via a Micro-USB cable,&lt;/span&gt;&lt;span&gt; communicates with the computer using the USB CDC class (Virtual COM Port).&lt;/span&gt;&lt;span&gt; Windows detects the board as a &lt;/span&gt;&lt;code data-path-to-node="24" data-index-in-node="163"&gt;COMx&lt;/code&gt;&lt;span&gt; port.&lt;/span&gt;&lt;span&gt; The microcontroller transmits measurement data in text (ASCII) format,&lt;/span&gt;&lt;span&gt; ending each frame with a carriage return/newline character (&lt;/span&gt;&lt;code data-path-to-node="24" data-index-in-node="305"&gt;\r&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="1"&gt;&lt;span&gt;Here are the key takeaways from the code analysis:&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1ju4" data-path-to-node="2"&gt;Serial Transmission Parameters (USB CDC)&lt;/h3&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;The &lt;/span&gt;&lt;code data-path-to-node="3" data-index-in-node="4"&gt;OT07Serial.cs&lt;/code&gt;&lt;span&gt; class shows how the host opens and configures the serial port:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="4"&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,0,0"&gt;&lt;b data-path-to-node="4,0,0" data-index-in-node="0"&gt;Baud Rate:&lt;/b&gt;&lt;span&gt; 115,&lt;/span&gt;&lt;span&gt;200 baud.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,1,0"&gt;&lt;b data-path-to-node="4,1,0" data-index-in-node="0"&gt;Frame Format:&lt;/b&gt;&lt;span&gt; 8 data bits,&lt;/span&gt;&lt;span&gt; no parity (&lt;/span&gt;&lt;code data-path-to-node="4,1,0" data-index-in-node="38"&gt;Parity.None&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; 1 stop bit (&lt;/span&gt;&lt;code data-path-to-node="4,1,0" data-index-in-node="64"&gt;StopBits.One&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,2,0"&gt;&lt;b data-path-to-node="4,2,0" data-index-in-node="0"&gt;Flow Control:&lt;/b&gt;&lt;span&gt; None (&lt;/span&gt;&lt;code data-path-to-node="4,2,0" data-index-in-node="20"&gt;Handshake.None&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,3,0"&gt;&lt;b data-path-to-node="4,3,0" data-index-in-node="0"&gt;Control Lines:&lt;/b&gt;&lt;span&gt; DTR and RTS signals are active (&lt;/span&gt;&lt;code data-path-to-node="4,3,0" data-index-in-node="47"&gt;DtrEnable = true&lt;/code&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="4,3,0" data-index-in-node="65"&gt;RtsEnable = true&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju5" data-path-to-node="5"&gt;Text Command Protocol (CLI / Firmware Communication)&lt;/h3&gt;
&lt;p data-path-to-node="6"&gt;&lt;span&gt;The application communicates with the microcontroller (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; MAX32630FTHR) using simple text commands sent and terminated with a carriage return (&lt;/span&gt;&lt;code data-path-to-node="6" data-index-in-node="146"&gt;\r&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt; Responses are also textual,&lt;/span&gt;&lt;span&gt; often with lines ending in newline characters.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="7"&gt;&lt;span&gt;Main commands used in the code:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="8"&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,0,0"&gt;&lt;code data-path-to-node="8,0,0" data-index-in-node="0"&gt;s&lt;/code&gt;&lt;span&gt; &amp;ndash; Device Search.&lt;/span&gt;&lt;span&gt; Returns the number of sensors found,&lt;/span&gt;&lt;span&gt; followed by lines with I&amp;sup2;C addresses and ROM codes.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,1,0"&gt;&lt;code data-path-to-node="8,1,0" data-index-in-node="0"&gt;t[index]&lt;/code&gt;&lt;span&gt; or &lt;/span&gt;&lt;code data-path-to-node="8,1,0" data-index-in-node="12"&gt;t [start] [end]&lt;/code&gt;&lt;span&gt; &amp;ndash; Temperature reading for a specific device index or a range of devices.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,2,0"&gt;&lt;code data-path-to-node="8,2,0" data-index-in-node="0"&gt;r[index] [addr]&lt;/code&gt;&lt;span&gt; or &lt;/span&gt;&lt;code data-path-to-node="8,2,0" data-index-in-node="19"&gt;r[index] [start_addr] [end_addr]&lt;/code&gt;&lt;span&gt; &amp;ndash; Read a register or a range of registers from a given I&amp;sup2;C sensor.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,3,0"&gt;&lt;code data-path-to-node="8,3,0" data-index-in-node="0"&gt;g[index] 0&lt;/code&gt;&lt;span&gt; &amp;ndash; Read all registers (register map) for the indicated device.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,4,0"&gt;&lt;code data-path-to-node="8,4,0" data-index-in-node="0"&gt;w[index] [addr] [data]&lt;/code&gt;&lt;span&gt; &amp;ndash; Write data (&lt;/span&gt;&lt;code data-path-to-node="8,4,0" data-index-in-node="37"&gt;data&lt;/code&gt;&lt;span&gt;) to a register (&lt;/span&gt;&lt;code data-path-to-node="8,4,0" data-index-in-node="58"&gt;addr&lt;/code&gt;&lt;span&gt;) of the selected sensor (&lt;/span&gt;&lt;code data-path-to-node="8,4,0" data-index-in-node="88"&gt;index&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju6" data-path-to-node="9"&gt;Response Format and Data Parsing&lt;/h3&gt;
&lt;p data-path-to-node="10"&gt;&lt;span&gt;In response to commands,&lt;/span&gt;&lt;span&gt; the firmware returns data enclosed in square brackets,&lt;/span&gt;&lt;span&gt; e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="10" data-index-in-node="86"&gt;[index] [value] [status]&lt;/code&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="11"&gt;
&lt;li&gt;
&lt;p data-path-to-node="11,0,0"&gt;&lt;b data-path-to-node="11,0,0" data-index-in-node="0"&gt;Temperature reading example:&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="11,0,0" data-index-in-node="29"&gt;[0] [23.500] [0]&lt;/code&gt;&lt;span&gt; (where the first is the device index,&lt;/span&gt;&lt;span&gt; the second is the temperature in &amp;deg;C,&lt;/span&gt;&lt;span&gt; and the third is the status &amp;ndash; e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="11,0,0" data-index-in-node="157"&gt;0&lt;/code&gt;&lt;span&gt; means successful connection / no errors).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="11,1,0"&gt;&lt;b data-path-to-node="11,1,0" data-index-in-node="0"&gt;Register values&lt;/b&gt;&lt;span&gt; are read in hexadecimal format (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="11,1,0" data-index-in-node="54"&gt;0x10&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju7" data-path-to-node="12"&gt;&amp;nbsp;MAX30208 Sensor Register Structure&lt;/h3&gt;
&lt;p data-path-to-node="13"&gt;&lt;span&gt;The &lt;/span&gt;&lt;code data-path-to-node="13" data-index-in-node="4"&gt;Form1.cs&lt;/code&gt;&lt;span&gt; class defines the complete register map of the MAX30208 chip (addresses from &lt;/span&gt;&lt;code data-path-to-node="13" data-index-in-node="90"&gt;0x00&lt;/code&gt;&lt;span&gt; to &lt;/span&gt;&lt;code data-path-to-node="13" data-index-in-node="98"&gt;0xFF&lt;/code&gt;&lt;span&gt;):&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="14"&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,0,0"&gt;&lt;b data-path-to-node="14,0,0" data-index-in-node="0"&gt;0x00 (STATUS):&lt;/b&gt;&lt;span&gt; Contains measurement readiness flags (&lt;/span&gt;&lt;code data-path-to-node="14,0,0" data-index-in-node="53"&gt;TEMP_RDY&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; alarm thresholds (&lt;/span&gt;&lt;code data-path-to-node="14,0,0" data-index-in-node="82"&gt;TEMP_HIGH&lt;/code&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="14,0,0" data-index-in-node="93"&gt;TEMP_LO&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; and FIFO buffer full status (&lt;/span&gt;&lt;code data-path-to-node="14,0,0" data-index-in-node="132"&gt;A_FULL&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,1,0"&gt;&lt;b data-path-to-node="14,1,0" data-index-in-node="0"&gt;0x01 (INTERRUPT ENABLE):&lt;/b&gt;&lt;span&gt; Configuration for enabling interrupts.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,2,0"&gt;&lt;b data-path-to-node="14,2,0" data-index-in-node="0"&gt;0x10 - 0x13:&lt;/b&gt;&lt;span&gt; High and low temperature alarm thresholds (MSB and LSB).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="14,3,0"&gt;&lt;b data-path-to-node="14,3,0" data-index-in-node="0"&gt;0x20 (TEMP SENSOR SETUP):&lt;/b&gt;&lt;span&gt; Contains the &lt;/span&gt;&lt;code data-path-to-node="14,3,0" data-index-in-node="39"&gt;CONVERT_T&lt;/code&gt;&lt;span&gt; bit (writing &lt;/span&gt;&lt;code data-path-to-node="14,3,0" data-index-in-node="62"&gt;1&lt;/code&gt;&lt;span&gt; triggers a single temperature measurement).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju8" data-path-to-node="15"&gt;Raw Temperature Data Conversion&lt;/h3&gt;
&lt;p data-path-to-node="16"&gt;&lt;span&gt;The code implements a method for converting 16-bit register values into degrees Celsius:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="17"&gt;
&lt;li&gt;
&lt;p data-path-to-node="17,0,0"&gt;&lt;span&gt;The sensor resolution is&amp;nbsp;&lt;/span&gt;&lt;span class="math-inline" data-index-in-node="25"&gt;0.005&lt;/span&gt;&lt;span&gt;&amp;nbsp;per bit (0.&lt;/span&gt;&lt;span&gt;005&amp;#39;C).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="17,1,0"&gt;&lt;span&gt;Negative values are handled by checking for a 16-bit overflow (&lt;/span&gt;&lt;span class="math-inline" data-index-in-node="63"&gt;32768&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="17,2,0"&gt;&lt;b data-path-to-node="17,2,0" data-index-in-node="0"&gt;Temperature formula:&lt;/b&gt;&lt;/p&gt;
&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Temperature.jpg" /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1ju9" data-path-to-node="19"&gt;How to use this knowledge in LabVIEW?&lt;/h3&gt;
&lt;p data-path-to-node="20"&gt;&lt;span&gt;When creating your own LabVIEW program to handle this kit via USB CDC,&lt;/span&gt;&lt;span&gt; instead of writing an advanced I&amp;sup2;C protocol parser from scratch,&lt;/span&gt;&lt;span&gt; you can send the exact same text commands to the serial port (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; sending the string &lt;/span&gt;&lt;code data-path-to-node="20" data-index-in-node="223"&gt;&amp;quot;s\r&amp;quot;&lt;/code&gt;&lt;span&gt; to detect devices or &lt;/span&gt;&lt;code data-path-to-node="20" data-index-in-node="250"&gt;&amp;quot;t0\r&amp;quot;&lt;/code&gt;&lt;span&gt; to read the temperature from device #0) and receive text responses using the &lt;/span&gt;&lt;b data-path-to-node="20" data-index-in-node="334"&gt;VISA Read&lt;/b&gt;&lt;span&gt; function.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="20"&gt;&lt;span style="font-size:150%;"&gt;Implementation process&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="1"&gt;&lt;span&gt;The implementation process carried out in LabVIEW consists of the following phases:&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1jua" data-path-to-node="2"&gt;Step 1: Serial Transmission Configuration (NI-VISA)&lt;/h3&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;Before reading data,&lt;/span&gt;&lt;span&gt; the serial port was correctly configured in LabVIEW using the &lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="83"&gt;VISA Configure Serial Port&lt;/b&gt;&lt;span&gt; function to match the parameters extracted from the C# source code (&lt;/span&gt;&lt;code data-path-to-node="3" data-index-in-node="178"&gt;OT07Serial.cs&lt;/code&gt;&lt;span&gt;):&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="4"&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,0,0"&gt;&lt;b data-path-to-node="4,0,0" data-index-in-node="0"&gt;Baud Rate:&lt;/b&gt;&lt;span&gt; 115,&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,1,0"&gt;&lt;b data-path-to-node="4,1,0" data-index-in-node="0"&gt;Data Bits:&lt;/b&gt;&lt;span&gt; 8&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,2,0"&gt;&lt;b data-path-to-node="4,2,0" data-index-in-node="0"&gt;Parity:&lt;/b&gt;&lt;span&gt; None&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,3,0"&gt;&lt;b data-path-to-node="4,3,0" data-index-in-node="0"&gt;Stop Bits:&lt;/b&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,4,0"&gt;&lt;b data-path-to-node="4,4,0" data-index-in-node="0"&gt;Flow Control:&lt;/b&gt;&lt;span&gt; None&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,5,0"&gt;&lt;b data-path-to-node="4,5,0" data-index-in-node="0"&gt;Termination Character:&lt;/b&gt;&lt;span&gt; Enabled (TRUE),&lt;/span&gt;&lt;span&gt; set to &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="46"&gt;\r&amp;nbsp;&lt;/span&gt;&lt;span&gt;(Return / 0x0D).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1jub" data-path-to-node="5"&gt;Step 2: LabVIEW Project Architecture (Block Diagram)&lt;/h3&gt;
&lt;p data-path-to-node="6"&gt;&lt;span&gt;A sequential logic structure was established to mirror the text command mechanism of the original application,&lt;/span&gt;&lt;span&gt; utilizing a &lt;/span&gt;&lt;b data-path-to-node="6" data-index-in-node="123"&gt;While Loop&lt;/b&gt;&lt;span&gt; for continuous data acquisition:&lt;/span&gt;&lt;/p&gt;
&lt;ol start="1" data-path-to-node="7"&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,0,0"&gt;&lt;b data-path-to-node="7,0,0" data-index-in-node="0"&gt;Initialization (Before the Loop):&lt;/b&gt;&lt;/p&gt;
&lt;ul data-path-to-node="7,0,1"&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,0,1,0,0"&gt;&lt;span&gt;The &lt;/span&gt;&lt;b data-path-to-node="7,0,1,0,0" data-index-in-node="4"&gt;VISA Open&lt;/b&gt;&lt;span&gt; function was used to open a connection to the selected COM port (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="7,0,1,0,0" data-index-in-node="85"&gt;COM3&lt;/code&gt;&lt;span&gt;).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,0,1,1,0"&gt;&lt;span&gt;The &lt;/span&gt;&lt;b data-path-to-node="7,0,1,1,0" data-index-in-node="4"&gt;VISA Configure Serial Port&lt;/b&gt;&lt;span&gt; function was applied with the parameters defined in Step 1.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,0"&gt;&lt;b data-path-to-node="7,1,0" data-index-in-node="0"&gt;Measurement Loop (While Loop Execution):&lt;/b&gt;&lt;/p&gt;
&lt;ul data-path-to-node="7,1,1"&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,1,0,0"&gt;&lt;b data-path-to-node="7,1,1,0,0" data-index-in-node="0"&gt;Command Transmission (VISA Write):&lt;/b&gt;&lt;span&gt; Sent text commands terminated with a carriage return \&lt;/span&gt;&lt;span class="math-inline" data-index-in-node="89"&gt;r&lt;/span&gt;&lt;span&gt;) using a string constant (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; sending &lt;/span&gt;&lt;code data-path-to-node="7,1,1,0,0" data-index-in-node="148"&gt;t0\r&lt;/code&gt;&lt;span&gt; to request a temperature reading from device index 0).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,1,1,0"&gt;&lt;b data-path-to-node="7,1,1,1,0" data-index-in-node="0"&gt;Response Acquisition (VISA Read):&lt;/b&gt;&lt;span&gt; Read the text buffer returned by the firmware,&lt;/span&gt;&lt;span&gt; which encapsulates output data inside square brackets (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="7,1,1,1,0" data-index-in-node="142"&gt;[0] [23.500] [0]&lt;/code&gt;&lt;span&gt; representing the device index,&lt;/span&gt;&lt;span&gt; temperature in &amp;deg;C,&lt;/span&gt;&lt;span&gt; and status code).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,1,2,0"&gt;&lt;b data-path-to-node="7,1,1,2,0" data-index-in-node="0"&gt;Data Parsing (Scan From String / Regex):&lt;/b&gt;&lt;span&gt; Processed the incoming string using pattern-matching functions to extract the numerical temperature value.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,1,3,0"&gt;&lt;b data-path-to-node="7,1,1,3,0" data-index-in-node="0"&gt;Data Visualization:&lt;/b&gt;&lt;span&gt; Routed the parsed temperature value to a numeric indicator and a Waveform Chart.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,1,4,0"&gt;&lt;b data-path-to-node="7,1,1,4,0" data-index-in-node="0"&gt;Loop Timing (Wait ms):&lt;/b&gt;&lt;span&gt; Integrated a 1,&lt;/span&gt;&lt;span&gt;000 ms (1 second) delay to match the native refresh rate of the original software and prevent 100% CPU utilization.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,2,0"&gt;&lt;b data-path-to-node="7,2,0" data-index-in-node="0"&gt;Resource Cleanup (After the Loop):&lt;/b&gt;&lt;/p&gt;
&lt;ul data-path-to-node="7,2,1"&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,2,1,0,0"&gt;&lt;span&gt;Upon pressing the stop button,&lt;/span&gt;&lt;span&gt; the loop terminates and the &lt;/span&gt;&lt;b data-path-to-node="7,2,1,0,0" data-index-in-node="59"&gt;VISA Close&lt;/b&gt;&lt;span&gt; function executes to properly release the COM port resource in the operating system.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="mcetoc_1k5qck1juc" data-path-to-node="8"&gt;Step 3: Advanced Command Support (Maxim Protocol Reproduction)&lt;/h3&gt;
&lt;p data-path-to-node="9"&gt;&lt;span&gt;To replicate the full feature set of the original application,&lt;/span&gt;&lt;span&gt; the LabVIEW architecture was expanded to handle multiple CLI commands:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="10"&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,0,0"&gt;&lt;b class="" data-path-to-node="10,0,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="10,0,0" data-index-in-node="0"&gt;s\r&lt;/code&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Initiates a device search across the I&amp;sup2;C bus,&lt;/span&gt;&lt;span&gt; returning the number of detected sensors along with their ROM codes and addresses.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,1,0"&gt;&lt;b class="" data-path-to-node="10,1,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="10,1,0" data-index-in-node="0"&gt;t0\r&lt;/code&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Requests a direct temperature conversion and reading from the sensor at index 0,&lt;/span&gt;&lt;span&gt; returning the formatted string &lt;/span&gt;&lt;code data-path-to-node="10,1,0" data-index-in-node="118"&gt;[0] [23.500] [0]&lt;/code&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,2,0"&gt;&lt;b class="" data-path-to-node="10,2,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="10,2,0" data-index-in-node="0"&gt;r0 10\r&lt;/code&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Reads register address &lt;/span&gt;&lt;code data-path-to-node="10,2,0" data-index-in-node="32"&gt;0x10&lt;/code&gt;&lt;span&gt; from device number 0,&lt;/span&gt;&lt;span&gt; returning the hexadecimal value.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,3,0"&gt;&lt;b class="" data-path-to-node="10,3,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="10,3,0" data-index-in-node="0"&gt;w0 10 1F\r&lt;/code&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Writes data (&lt;/span&gt;&lt;code data-path-to-node="10,3,0" data-index-in-node="25"&gt;0x1F&lt;/code&gt;&lt;span&gt;) into register &lt;/span&gt;&lt;code data-path-to-node="10,3,0" data-index-in-node="45"&gt;0x10&lt;/code&gt;&lt;span&gt; of device number 0,&lt;/span&gt;&lt;span&gt; yielding a confirmation response.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1jud" data-path-to-node="11"&gt;Best Practices Implemented:&lt;/h3&gt;
&lt;ul data-path-to-node="12"&gt;
&lt;li&gt;
&lt;p data-path-to-node="12,0,0"&gt;&lt;b data-path-to-node="12,0,0" data-index-in-node="0"&gt;Error Handling (Error Cluster):&lt;/b&gt;&lt;span&gt; The error cluster wire is daisy-chained continuously through every VISA operation (&lt;/span&gt;&lt;code data-path-to-node="12,0,0" data-index-in-node="115"&gt;Open&lt;/code&gt;&amp;nbsp;-&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code data-path-to-node="12,0,0" data-index-in-node="132"&gt;Configure&lt;/code&gt;&lt;span&gt; &lt;/span&gt;-&amp;gt;&amp;nbsp;&lt;span&gt;&lt;/span&gt;&lt;code data-path-to-node="12,0,0" data-index-in-node="154"&gt;Write/Read&lt;/code&gt;&lt;span&gt; &lt;/span&gt;&lt;span class="math-inline" data-index-in-node="165"&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;code data-path-to-node="12,0,0" data-index-in-node="177"&gt;Close&lt;/code&gt;&lt;span&gt;) to ensure robust exception handling and prevent application freezing if the USB cable is abruptly disconnected.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p class="first-token" data-path-to-node="0"&gt;&lt;span class=""&gt;To display real-time temperature data from the &lt;/span&gt;&lt;b data-path-to-node="0" data-index-in-node="47"&gt;MAX30208EVSYS&lt;/b&gt;&lt;span&gt; sensor on a LabVIEW chart,&lt;/span&gt;&lt;span&gt; the &lt;/span&gt;&lt;b data-path-to-node="0" data-index-in-node="92"&gt;Waveform Chart&lt;/b&gt;&lt;span&gt; element is the recommended choice.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="1"&gt;&lt;span&gt;The configuration process in the project involves the following steps:&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1jue" data-path-to-node="2"&gt;1. Front Panel Configuration&lt;/h3&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;From the &lt;/span&gt;&lt;b data-path-to-node="3" data-index-in-node="9"&gt;Controls Palette&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; navigate to &lt;/span&gt;&lt;b class="" data-path-to-node="3" data-index-in-node="39"&gt;Graph -&amp;gt;&amp;nbsp;Waveform Chart&lt;/b&gt;&lt;span&gt; and place it on your user interface:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="4"&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,0,0"&gt;&lt;b data-path-to-node="4,0,0" data-index-in-node="0"&gt;Chart vs. Graph Selection:&lt;/b&gt;&lt;span&gt; The &lt;/span&gt;&lt;i data-path-to-node="4,0,0" data-index-in-node="31"&gt;Waveform Chart&lt;/i&gt;&lt;span&gt; was chosen over a standard Graph because it automatically maintains a history buffer of incoming data points and scrolls over time.&lt;/span&gt;&lt;span&gt; A Graph,&lt;/span&gt;&lt;span&gt; by contrast,&lt;/span&gt;&lt;span&gt; would require manual array management to store and update historical data with every new temperature reading.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,1,0"&gt;&lt;b data-path-to-node="4,1,0" data-index-in-node="0"&gt;Labeling:&lt;/b&gt;&lt;span&gt; The indicator is designated as &lt;/span&gt;&lt;i data-path-to-node="4,1,0" data-index-in-node="41"&gt;Temperature History&lt;/i&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="4,2,0"&gt;&lt;b data-path-to-node="4,2,0" data-index-in-node="0"&gt;Axis Precision:&lt;/b&gt;&lt;span&gt; Because the MAX30208 sensor provides high-precision data down to thousandths of a degree,&lt;/span&gt;&lt;span&gt; the Y-axis formatting was adjusted via &lt;/span&gt;&lt;b class="" data-path-to-node="4,2,0" data-index-in-node="145"&gt;Right-Click -&amp;gt;&amp;nbsp;Format and Precision&lt;/b&gt;&lt;span&gt; (setting &lt;/span&gt;&lt;i data-path-to-node="4,2,0" data-index-in-node="199"&gt;Digits of Precision&lt;/i&gt;&lt;span&gt; to at least 3) to properly display floating-point values with multiple decimal places.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Front-Panel.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1juf" data-path-to-node="5"&gt;2. Block Diagram Implementation&lt;/h3&gt;
&lt;p data-path-to-node="6"&gt;&lt;span&gt;Inside the primary &lt;/span&gt;&lt;b data-path-to-node="6" data-index-in-node="19"&gt;While Loop&lt;/b&gt;&lt;span&gt; where data is acquired from the device:&lt;/span&gt;&lt;/p&gt;
&lt;ol start="1" data-path-to-node="7"&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,0,0"&gt;&lt;b data-path-to-node="7,0,0" data-index-in-node="0"&gt;Data Acquisition &amp;amp; Parsing:&lt;/b&gt;&lt;span&gt; After receiving the serial string via &lt;/span&gt;&lt;b data-path-to-node="7,0,0" data-index-in-node="66"&gt;VISA Read&lt;/b&gt;&lt;span&gt; (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="7,0,0" data-index-in-node="83"&gt;[0] [23.500] [0]&lt;/code&gt;&lt;span&gt;) and extracting the numeric temperature value as a Double-precision float,&lt;/span&gt;&lt;span&gt; a single sample is ready for plotting.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="7,1,0"&gt;&lt;b data-path-to-node="7,1,0" data-index-in-node="0"&gt;Data Wire Connection:&lt;/b&gt;&lt;span&gt; The numeric data wire is connected directly to the Waveform Chart terminal located on the loop boundary (or inside it).&lt;/span&gt;&lt;span&gt; LabVIEW automatically appends the new sample to the chart&amp;#39;s internal history buffer.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/Block-Diagram.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5qck1jug" data-path-to-node="8"&gt;3. Timeline and Smoothness Optimization&lt;/h3&gt;
&lt;p data-path-to-node="9"&gt;&lt;span&gt;To ensure the chart clearly visualizes temperature variations over time:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="10"&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,0,0"&gt;&lt;b data-path-to-node="10,0,0" data-index-in-node="0"&gt;Loop Timing (Wait ms):&lt;/b&gt;&lt;span&gt; A &lt;/span&gt;&lt;b data-path-to-node="10,0,0" data-index-in-node="25"&gt;Wait (ms)&lt;/b&gt;&lt;span&gt; function is embedded inside the While Loop,&lt;/span&gt;&lt;span&gt; configured to a 1,&lt;/span&gt;&lt;span&gt;000 ms delay (1 second per reading),&lt;/span&gt;&lt;span&gt; matching the default refresh rate of the native application.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,1,0"&gt;&lt;b data-path-to-node="10,1,0" data-index-in-node="0"&gt;X-Axis (Time Scale):&lt;/b&gt;&lt;span&gt; Through &lt;/span&gt;&lt;b class="" data-path-to-node="10,1,0" data-index-in-node="29"&gt;PropertiesScales&amp;nbsp;Time&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; the time formatting can be customized (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; displaying hours,&lt;/span&gt;&lt;span&gt; minutes,&lt;/span&gt;&lt;span&gt; and seconds).&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,2,0"&gt;&lt;b data-path-to-node="10,2,0" data-index-in-node="0"&gt;History Length:&lt;/b&gt;&lt;span&gt; By default,&lt;/span&gt;&lt;span&gt; a Waveform Chart retains the last 1,&lt;/span&gt;&lt;span&gt;000 points.&lt;/span&gt;&lt;span&gt; To extend the retention window (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; covering several hours of data),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;b data-path-to-node="10,2,0" data-index-in-node="147"&gt;Data History&lt;/b&gt;&lt;span&gt; can be modified by right-clicking the chart and increasing the buffer size.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5qck1juh" data-path-to-node="2"&gt;4. Sensor Communication in LabVIEW&lt;/h3&gt;
&lt;p data-path-to-node="3"&gt;&lt;span&gt;The MAX30208EVSYS evaluation board streams a raw text frame over the virtual COM port whenever a measurement command is triggered.&lt;/span&gt;&lt;span&gt; The received data string typically follows this format:&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="4"&gt;&lt;code data-path-to-node="4" data-index-in-node="0"&gt;device[00] temperature[18.090] status[0]&lt;/code&gt;&lt;/p&gt;
&lt;p data-path-to-node="5"&gt;&lt;span&gt;To successfully parse this string and extract the temperature value in LabVIEW,&lt;/span&gt;&lt;span&gt; the implementation follows these steps:&lt;/span&gt;&lt;/p&gt;
&lt;ol start="1" data-path-to-node="6"&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,0,0"&gt;&lt;b data-path-to-node="6,0,0" data-index-in-node="0"&gt;Trimming Hidden Characters:&lt;/b&gt;&lt;span&gt; Serial devices often append hidden carriage return and line feed characters (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="105"&gt;\r\n&lt;/code&gt;&lt;span&gt;) at the beginning or end of the incoming data stream.&lt;/span&gt;&lt;span&gt; Before any parsing takes place,&lt;/span&gt;&lt;span&gt; the raw string is passed through LabVIEW&amp;rsquo;s &lt;/span&gt;&lt;b data-path-to-node="6,0,0" data-index-in-node="239"&gt;Trim Whitespace&lt;/b&gt;&lt;span&gt; function.&lt;/span&gt;&lt;span&gt; Trimming the text removes these stray control characters and whitespace,&lt;/span&gt;&lt;span&gt; preventing matching errors and ensuring the string format is clean and predictable.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,1,0"&gt;&lt;b data-path-to-node="6,1,0" data-index-in-node="0"&gt;Parsing and String Matching:&lt;/b&gt;&lt;span&gt; Once the text is properly trimmed,&lt;/span&gt;&lt;span&gt; LabVIEW uses pattern matching or the &lt;/span&gt;&lt;b data-path-to-node="6,1,0" data-index-in-node="101"&gt;Scan From String&lt;/b&gt;&lt;span&gt; function to locate the &lt;/span&gt;&lt;code data-path-to-node="6,1,0" data-index-in-node="141"&gt;temperature[...]&lt;/code&gt;&lt;span&gt; block.&lt;/span&gt;&lt;span&gt; It isolates the characters nested inside the square brackets.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,2,0"&gt;&lt;b data-path-to-node="6,2,0" data-index-in-node="0"&gt;String-to-Number Conversion:&lt;/b&gt;&lt;span&gt; The isolated floating-point number (e.&lt;/span&gt;&lt;span&gt;g.,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;code data-path-to-node="6,2,0" data-index-in-node="71"&gt;18.090&lt;/code&gt;&lt;span&gt;) is converted from text into a numeric data type (Double format).&lt;/span&gt;&lt;span&gt; This yields a clean numerical temperature value ready to be displayed on indicators,&lt;/span&gt;&lt;span&gt; plotted on real-time charts,&lt;/span&gt;&lt;span&gt; fed into the statistical analysis engine,&lt;/span&gt;&lt;span&gt; or logged to a CSV file.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/TermometerLabView.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/LabView.mp4:640:360]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;</description></item><item><title>VitalSense – Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt</title><link>https://community.element14.com/thread/57386?ContentTypeID=0</link><pubDate>Sun, 25 Oct 2026 23:26:38 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:d36b2af3-dcd4-4171-8886-f2913eb68dae</guid><dc:creator>jelektro</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/57386?ContentTypeID=0</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57386/vitalsense-part-13-dual-port-biomedical-telemetry-dashboard---integrating-stm32-maxrefdes117-and-max30208evsys-in-c-and-qt/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p data-path-to-node="2"&gt;&lt;span class="" style="font-size:150%;" data-path-to-node="2,0"&gt;Table of Contents&lt;br /&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57368/vitalsense-part-1-system-concept-and-architecture" data-e14adj="t"&gt;Part 1: System Concept and Architecture&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57369/vitalsense-part-2-sponsored-kit-overview" data-e14adj="t"&gt;Part 2 : Sponsored Kit Overview&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57370/vitalsense-part-3-alternatives-for-heart-rate-spo2-and-temperature-sensing" data-e14adj="t"&gt;Part 3 : Alternatives for Heart Rate, SpO₂, and Temperature Sensing&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57371/vitalsense-part-4-max30208evsys-evaluation-system-technical-overview-operation" data-e14adj="t"&gt;Part 4 : MAX30208EVSYS Evaluation System &amp;ndash; Technical Overview &amp;amp; Operation&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57372/vitalsense-part-5-reverse-engineering-the-max30208evsys-communication-protocol" data-e14adj="t"&gt;Part 5 : Reverse-Engineering the MAX30208EVSYS Communication Protocol&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57381/vitalsense-part-6-communicating-with-the-max30208evsys-in-labview" data-e14adj="t"&gt;Part 6 : Communicating with the MAX30208EVSYS in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57382/vitalsense-part-7-building-a-cross-platform-qt6-desktop-application-for-the-max30208evsys-temperature-sensor" data-e14adj="t"&gt;Part 7 : Building a Cross-Platform Qt6 Desktop Application for the MAX30208EVSYS Temperature Sensor&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57383/vitalsense-part-8-stm32-pulse-oximeter-heart-rate-monitor-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 8 : STM32 Pulse Oximeter &amp;amp; Heart Rate Monitor (MAXREFDES117 + USB CDC)&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57389/vitalsense-part-9-maxrefdes117-json-data-acquisition-via-usb-cdc-in-labview" data-e14adj="t"&gt;Part 9 : MAXREFDES117 &amp;ndash; JSON Data Acquisition via USB-CDC in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57384/vitalsense-part-10-building-a-real-time-sensor-data-visualizer-for-maxrefdes117-usb-cdc" data-e14adj="t"&gt;Part 10 : Building a Real-Time Sensor Data Visualizer for MAXREFDES117 / USB CDC&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57385/vitalsense-part-11-research-workstation-system-architecture" data-e14adj="t"&gt;Part 11 : Research Workstation System Architecture&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57390/vitalsense-part-12-combined-pulse-oximeter-temperature-monitoring-system-in-labview" data-e14adj="t"&gt;Part 12 : Combined Pulse Oximeter &amp;amp; Temperature Monitoring System in LabVIEW&lt;br /&gt;&lt;/a&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;a href="https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57386/vitalsense-part-13-dual-port-biomedical-telemetry-dashboard---integrating-stm32-maxrefdes117-and-max30208evsys-in-c-and-qt" data-e14adj="t"&gt;Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="2"&gt;&lt;span class="" data-path-to-node="2,0"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id="p-rc_496d65b345f9f6f1-136" data-path-to-node="2"&gt;&lt;span class="" data-path-to-node="2,0"&gt;In this comprehensive blog post, we will explore how to design and implement a robust, multi-threaded cross-platform desktop application using &lt;b data-path-to-node="2,0" data-index-in-node="143"&gt;C++&lt;/b&gt; and &lt;b data-path-to-node="2,0" data-index-in-node="151"&gt;Qt 6&lt;/b&gt;. This application manages &lt;b data-path-to-node="2,0" data-index-in-node="182"&gt;two independent serial ports simultaneously&lt;/b&gt;, parses real-time JSON payloads, implements the authentic MAX30208 command-line protocol&lt;/span&gt;&lt;span class="" data-path-to-node="2,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="2,2"&gt;, and renders responsive, live multi-parameter charts.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="2"&gt;&lt;span data-path-to-node="2,2"&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/DualSensor-GUI-1.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="2"&gt;&lt;span data-path-to-node="2,2"&gt;[View:/cfs-file/__key/communityserver-discussions-components-files/458/DualSensor-GUI.mp4:640:360]&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="1"&gt;&lt;span&gt; A prime example of this engineering feat is a robust,&lt;/span&gt;&lt;span&gt; cross-platform desktop application developed in C++ and Qt 6 designed to simultaneously monitor,&lt;/span&gt;&lt;span&gt; parse,&lt;/span&gt;&lt;span&gt; and visualize telemetry data from two independent hardware sensor modules over separate serial ports.&lt;/span&gt;&lt;/p&gt;
&lt;p id="p-rc_72d725c80407277b-129" data-path-to-node="2"&gt;&lt;span class="" data-path-to-node="2,0"&gt;At the core of this system lies a sophisticated &lt;b data-path-to-node="2,0" data-index-in-node="48"&gt;Dual-Port Asynchronous Architecture&lt;/b&gt;. The STM32 / MAXREFDES117 stream operates asynchronously on the main GUI thread via &lt;code data-path-to-node="2,0" data-index-in-node="168"&gt;QSerialPort::readyRead&lt;/code&gt;, continuously capturing and parsing high-frequency JSON packets containing Heart Rate (&lt;code data-path-to-node="2,0" data-index-in-node="278"&gt;bpm&lt;/code&gt;), Blood Oxygen (&lt;code data-path-to-node="2,0" data-index-in-node="298"&gt;spo2&lt;/code&gt;), and Filtered Infrared (&lt;code data-path-to-node="2,0" data-index-in-node="328"&gt;filtered_ir&lt;/code&gt;) signals. In parallel, the MAX30208EVSYS protocol link is managed inside a dedicated background &lt;code data-path-to-node="2,0" data-index-in-node="436"&gt;QThread&lt;/code&gt; using a thread-safe &lt;code data-path-to-node="2,0" data-index-in-node="464"&gt;SerialWorker&lt;/code&gt; class&lt;/span&gt;&lt;span class="" data-path-to-node="2,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,2"&gt;. This module communicates via a command-based protocol (&lt;code data-path-to-node="2,2" data-index-in-node="57"&gt;s&lt;/code&gt;, &lt;code data-path-to-node="2,2" data-index-in-node="60"&gt;t&lt;/code&gt;, &lt;code data-path-to-node="2,2" data-index-in-node="63"&gt;r&lt;/code&gt;, &lt;code data-path-to-node="2,2" data-index-in-node="66"&gt;w&lt;/code&gt;)&lt;/span&gt;&lt;span class="" data-path-to-node="2,3"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,4"&gt; to fetch precise thermal metrics via a non-blocking &lt;code data-path-to-node="2,4" data-index-in-node="53"&gt;QTimer&lt;/code&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="2,5"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="2,6"&gt;, effectively isolating blocking operations and maintaining optimal UI frame rates.&lt;/span&gt;&lt;/p&gt;
&lt;p id="p-rc_72d725c80407277b-130" data-path-to-node="3"&gt;&lt;span class="" data-path-to-node="3,0"&gt;Beyond backend communication, the application excels in &lt;b data-path-to-node="3,0" data-index-in-node="56"&gt;Real-Time Multi-Parameter Visualization&lt;/b&gt; powered by Qt Charts. It features four independent live charts tracking BPM, SpO2, Filtered IR, and Temperature&lt;/span&gt;&lt;span class="" data-path-to-node="3,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="" data-path-to-node="3,2"&gt;, all equipped with a sliding-window buffer that automatically shifts the X-axis after a set threshold of samples. To augment the graphical representation, a vertical thermometer progress bar provides operators with instant visual feedback on thermal variations. Furthermore, &lt;b data-path-to-node="3,2" data-index-in-node="276"&gt;Dynamic UI Layout Customization&lt;/b&gt; empowers users to switch the dashboard layout at runtime between a Vertically Stacked arrangement and a 2x2 Side-by-Side Grid layout using an intuitive dropdown control.&lt;/span&gt;&lt;/p&gt;
&lt;p id="p-rc_72d725c80407277b-131" data-path-to-node="4"&gt;&lt;span class="" data-path-to-node="4,0"&gt;Usability and data management are further elevated through thoughtful software design patterns. The application incorporates &lt;b data-path-to-node="4,0" data-index-in-node="125"&gt;Bilingual Localization (PL / EN)&lt;/b&gt;, implementing seamless runtime language switching between Polish and English via a dynamic retranslation mechanism (&lt;code data-path-to-node="4,0" data-index-in-node="274"&gt;retranslateUi&lt;/code&gt;). This instantly updates all interface elements, tooltips, buttons, and chart titles. Concurrently, automated data persistence is handled by &lt;code data-path-to-node="4,0" data-index-in-node="429"&gt;CsvLogger&lt;/code&gt;, which automatically initiates a timestamped CSV session upon successful device connection, logging elapsed time, device indices, temperatures, and hardware status flags for subsequent post-processing analysis&lt;/span&gt;&lt;span class="" data-path-to-node="4,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="4,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="mcetoc_1k5r2u4bk0" data-path-to-node="4"&gt;Architectural Overview: Two Independent Data Streams&lt;/h2&gt;
&lt;p data-path-to-node="5"&gt;&lt;span&gt;To ensure low latency,&lt;/span&gt;&lt;span&gt; thread safety,&lt;/span&gt;&lt;span&gt; and freedom from data blockages,&lt;/span&gt;&lt;span&gt; the application architecture splits telemetry handling into two distinct pathways:&lt;/span&gt;&lt;/p&gt;
&lt;ol start="1" data-path-to-node="6"&gt;
&lt;li&gt;
&lt;p data-path-to-node="6,0,0"&gt;&lt;b data-path-to-node="6,0,0" data-index-in-node="0"&gt;The Sensor Stream (STM32 / MAXREFDES117):&lt;/b&gt;&lt;span&gt; Configured via standard USB CDC (115200 baud) nad&lt;/span&gt;&lt;span&gt;&amp;nbsp;operates asynchronously on the main GUI thread via &lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="80"&gt;QSerialPort::readyRead&lt;/code&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; continuously capturing and parsing high-frequency JSON packets containing Heart Rate (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="190"&gt;bpm&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; Blood Oxygen (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="210"&gt;spo2&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; and Filtered Infrared (&lt;/span&gt;&lt;code data-path-to-node="6,0,0" data-index-in-node="240"&gt;filtered_ir&lt;/code&gt;&lt;span&gt;) signals.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p id="p-rc_496d65b345f9f6f1-137" data-path-to-node="6,1,0"&gt;&lt;span class="" data-path-to-node="6,1,0,0"&gt;&lt;b data-path-to-node="6,1,0,0" data-index-in-node="0"&gt;The Temperature Protocol Link (MAX30208EVSYS):&lt;/b&gt; Based on the evaluation kit&amp;#39;s firmware protocol (operating at 115200 baud, 8 data bits, no parity, 1 stop bit)&lt;/span&gt;&lt;span class="" data-path-to-node="6,1,0,2"&gt;.&amp;nbsp;&lt;/span&gt;&lt;span class="" data-path-to-node="6,1,0,4"&gt;Managed inside a dedicated background &lt;code data-path-to-node="6,1,0,0" data-index-in-node="67"&gt;QThread&lt;/code&gt; using a thread-safe &lt;code data-path-to-node="6,1,0,0" data-index-in-node="95"&gt;SerialWorker&lt;/code&gt; class&lt;span&gt;&lt;/span&gt;. It communicates via a command-based protocol (&lt;code data-path-to-node="6,1,0,2" data-index-in-node="48"&gt;s&lt;/code&gt;, &lt;code data-path-to-node="6,1,0,2" data-index-in-node="51"&gt;t&lt;/code&gt;, &lt;code data-path-to-node="6,1,0,2" data-index-in-node="54"&gt;r&lt;/code&gt;, &lt;code data-path-to-node="6,1,0,2" data-index-in-node="57"&gt;w&lt;/code&gt;)&lt;span&gt;&lt;/span&gt; to fetch precise thermal metrics via a non-blocking &lt;code data-path-to-node="6,1,0,4" data-index-in-node="53"&gt;QTimer&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="mcetoc_1k5r2u4bk1" data-path-to-node="8"&gt;Technical Implementation&lt;/h2&gt;
&lt;h3 id="mcetoc_1k5r2u4bk2" data-path-to-node="9"&gt;1. Robust Serial Port Management and Enumeration&lt;/h3&gt;
&lt;p id="p-rc_496d65b345f9f6f1-138" data-path-to-node="10"&gt;&lt;span class="" data-path-to-node="10,0"&gt;The application leverages hardware identification filters to locate devices accurately. For the MAX30208 evaluation kit, the serial layer looks up specific vendor and product IDs (&lt;code data-path-to-node="10,0" data-index-in-node="180"&gt;VID_0B6A&lt;/code&gt; and &lt;code data-path-to-node="10,0" data-index-in-node="193"&gt;PID_0042&lt;/code&gt;) using Windows Management Instrumentation (WMI) queries, ensuring reliable plug-and-play identification&lt;/span&gt;&lt;span class="" data-path-to-node="10,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="10,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r2u4bk3" class="" data-path-to-node="11"&gt;2. Implementing the MAX30208 Evaluation Kit Protocol&lt;/h3&gt;
&lt;p data-path-to-node="12"&gt;&lt;span&gt;Derived directly from the evaluation kit&amp;#39;s firmware communication structure,&lt;/span&gt;&lt;span&gt; the custom &lt;/span&gt;&lt;code data-path-to-node="12" data-index-in-node="88"&gt;OT07Serial&lt;/code&gt;&lt;span&gt; class maps all core hardware interaction commands:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="13"&gt;
&lt;li&gt;
&lt;p id="p-rc_496d65b345f9f6f1-139" data-path-to-node="13,0,0"&gt;&lt;span class="" data-path-to-node="13,0,0,0"&gt;&lt;b data-path-to-node="13,0,0,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="13,0,0,0" data-index-in-node="0"&gt;s&lt;/code&gt; (Search Command):&lt;/b&gt; Scans the I&amp;sup2;C bus for connected sensors, returning the device count, I&amp;sup2;C addresses, and unique 64-bit ROM codes formatted inside bracketed strings (e.g., &lt;code data-path-to-node="13,0,0,0" data-index-in-node="174"&gt;[Index] [Address] [RomCode]&lt;/code&gt;)&lt;/span&gt;&lt;span class="" data-path-to-node="13,0,0,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="13,0,0,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p id="p-rc_496d65b345f9f6f1-140" data-path-to-node="13,1,0"&gt;&lt;span class="" data-path-to-node="13,1,0,0"&gt;&lt;b data-path-to-node="13,1,0,0" data-index-in-node="0"&gt;&lt;code data-path-to-node="13,1,0,0" data-index-in-node="0"&gt;t&lt;/code&gt; (Temperature Read):&lt;/b&gt; Requests active temperature conversions for individual or ranged device indices, parsing floating-point Celsius values and status flags&lt;/span&gt;&lt;span class="" data-path-to-node="13,1,0,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="13,1,0,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p id="p-rc_496d65b345f9f6f1-141" data-path-to-node="13,2,0"&gt;&lt;span class="" data-path-to-node="13,2,0,0"&gt;&lt;b data-path-to-node="13,2,0,0" data-index-in-node="0"&gt;Register Control (&lt;code data-path-to-node="13,2,0,0" data-index-in-node="18"&gt;r&lt;/code&gt;, &lt;code data-path-to-node="13,2,0,0" data-index-in-node="21"&gt;w&lt;/code&gt;, &lt;code data-path-to-node="13,2,0,0" data-index-in-node="24"&gt;g&lt;/code&gt;):&lt;/b&gt; Provides low-level read, write, and bulk configuration dumps across the 24-register memory map of the MAX30208 sensor&lt;/span&gt;&lt;span class="" data-path-to-node="13,2,0,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="13,2,0,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5r2u4bk4" class="" data-path-to-node="14"&gt;3. Real-Time Multi-Chart Visualization (&lt;code data-path-to-node="14" data-index-in-node="40"&gt;Qt Charts&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-path-to-node="15"&gt;&lt;span&gt;Plotting high-frequency physiological signals alongside low-frequency thermal data on a single axis causes scaling distortion.&lt;/span&gt;&lt;span&gt; To solve this,&lt;/span&gt;&lt;span&gt; the application provisions &lt;/span&gt;&lt;b data-path-to-node="15" data-index-in-node="169"&gt;three independent chart panels&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;ul data-path-to-node="16"&gt;
&lt;li&gt;
&lt;p data-path-to-node="16,0,0"&gt;&lt;b data-path-to-node="16,0,0" data-index-in-node="0"&gt;Rolling Window Buffer:&lt;/b&gt;&lt;span&gt; A sample counter tracks incoming frames.&lt;/span&gt;&lt;span&gt; Once limits exceed 100 samples,&lt;/span&gt;&lt;span&gt; the X-axis dynamically shifts (&lt;/span&gt;&lt;code data-path-to-node="16,0,0" data-index-in-node="127"&gt;axisX-&amp;gt;setRange(...)&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; creating a clean,&lt;/span&gt;&lt;span&gt; sliding oscilloscope effect.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="16,1,0"&gt;&lt;b data-path-to-node="16,1,0" data-index-in-node="0"&gt;Layout Adaptability:&lt;/b&gt;&lt;span&gt; Users can instantly switch between a vertical stacked layout and a side-by-side horizontal arrangement via an intuitive dropdown menu.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p id="p-rc_9507160a90c37109-122" data-path-to-node="8,0,0"&gt;&lt;span class="" data-path-to-node="8,0,0,0"&gt;Features &lt;b data-path-to-node="8,0,0,0" data-index-in-node="9"&gt;four independent live charts&lt;/b&gt; (BPM, SpO2, Filtered IR, and Temperature)&lt;/span&gt;&lt;span class="" data-path-to-node="8,0,0,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="8,0,0,2"&gt; equipped with a sliding-window buffer that automatically shifts the X-axis after a set threshold of samples.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-path-to-node="8,1,0"&gt;&lt;span&gt;Includes a vertical &lt;/span&gt;&lt;b data-path-to-node="8,1,0" data-index-in-node="20"&gt;thermometer progress bar&lt;/b&gt;&lt;span&gt; providing instant visual feedback on thermal variations.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5r3bl476" data-path-to-node="9"&gt;4. Dynamic UI Layout Customization&lt;/h3&gt;
&lt;ul data-path-to-node="10"&gt;
&lt;li&gt;
&lt;p data-path-to-node="10,0,0"&gt;&lt;span&gt;Users can switch the dashboard layout at runtime between a &lt;/span&gt;&lt;b data-path-to-node="10,0,0" data-index-in-node="59"&gt;Vertically Stacked&lt;/b&gt;&lt;span&gt; arrangement and a &lt;/span&gt;&lt;b data-path-to-node="10,0,0" data-index-in-node="96"&gt;2x2 Side-by-Side Grid&lt;/b&gt;&lt;span&gt; layout using an intuitive dropdown control.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/DualSensor-GUI-1.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img style="max-height:360px;max-width:640px;" alt=" " src="https://community.element14.com/resized-image/__size/1280x720/__key/communityserver-discussions-components-files/458/DualSensor-GUI-2.jpg" /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id="mcetoc_1k5r3bl477" data-path-to-node="11"&gt;5. Bilingual Localization (PL / EN)&lt;/h3&gt;
&lt;ul data-path-to-node="12"&gt;
&lt;li&gt;
&lt;p data-path-to-node="12,0,0"&gt;&lt;span&gt;Implements seamless &lt;/span&gt;&lt;b data-path-to-node="12,0,0" data-index-in-node="20"&gt;runtime language switching&lt;/b&gt;&lt;span&gt; between Polish and English using a dynamic retranslation mechanism (&lt;/span&gt;&lt;code data-path-to-node="12,0,0" data-index-in-node="115"&gt;retranslateUi&lt;/code&gt;&lt;span&gt;),&lt;/span&gt;&lt;span&gt; instantly updating all interface elements,&lt;/span&gt;&lt;span&gt; tooltips,&lt;/span&gt;&lt;span&gt; buttons,&lt;/span&gt;&lt;span&gt; and chart titles.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mcetoc_1k5r3bl478" class="" data-path-to-node="13"&gt;6. Automated Data Logging (&lt;code data-path-to-node="13" data-index-in-node="27"&gt;CsvLogger&lt;/code&gt;)&lt;/h3&gt;
&lt;ul data-path-to-node="14"&gt;
&lt;li&gt;
&lt;p id="p-rc_9507160a90c37109-123" data-path-to-node="14,0,0"&gt;&lt;span data-path-to-node="14,0,0,0"&gt;Automatically initiates a timestamped CSV session upon successful device connection, logging elapsed time, device indices, temperatures, and hardware status flags for post-processing analysis&lt;/span&gt;&lt;span class="" data-path-to-node="14,0,0,1"&gt;&lt;span&gt;&lt;sup class="superscript"&gt;&lt;/sup&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node="14,0,0,2"&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mcetoc_1k5r2u4bk5" data-path-to-node="18"&gt;&amp;nbsp;Conclusion&lt;/h2&gt;
&lt;p data-path-to-node="19"&gt;&lt;span&gt;Integrating disparate hardware protocols into a single,&lt;/span&gt;&lt;span&gt; cohesive desktop environment highlights the flexibility of modern C++ and Qt.&lt;/span&gt;&lt;span&gt; By separating asynchronous JSON data acquisition from polled command-line protocols,&lt;/span&gt;&lt;span&gt; developers can scale telemetry systems easily&amp;mdash;transforming raw microcontroller evaluation kits into professional-grade diagnostic dashboards.&lt;/span&gt;&lt;/p&gt;</description></item><item><title>RE: VitalSense – Part 13 : Dual-Port Biomedical Telemetry Dashboard - Integrating STM32 (MAXREFDES117) and MAX30208EVSYS in C++ and Qt</title><link>https://community.element14.com/thread/238775?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 07:56:02 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:338bee0a-f5dc-4a9b-801b-04cc5915144c</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238775?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57386/vitalsense-part-13-dual-port-biomedical-telemetry-dashboard---integrating-stm32-maxrefdes117-and-max30208evsys-in-c-and-qt/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Nice to see you using Qt for the interface!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitaRF - Part 9 - Sleep and Fall Detection</title><link>https://community.element14.com/thread/238774?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 07:52:21 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:a1de018a-9fd7-448c-8c90-d23f7c5f68bf</guid><dc:creator>Qbit</dc:creator><slash:comments>1</slash:comments><comments>https://community.element14.com/thread/238774?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57388/vitarf---part-9---sleep-and-fall-detection/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Excellent post! Using the LIS2DE12 FIFO and integer arithmetic makes both sleep actigraphy and fall detection super efficient on the nRF52. Good to see your grandma helping in the data collection.&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item><item><title>RE: VitalSense – Part 9 : MAXREFDES117 – JSON Data Acquisition via USB-CDC in LabVIEW</title><link>https://community.element14.com/thread/238773?ContentTypeID=1</link><pubDate>Mon, 26 Oct 2026 07:47:02 GMT</pubDate><guid isPermaLink="false">93d5dcb4-84c2-446f-b2cb-99731719e767:3cb505f4-e906-4f8a-b2da-8fc36e848675</guid><dc:creator>Qbit</dc:creator><slash:comments>0</slash:comments><comments>https://community.element14.com/thread/238773?ContentTypeID=1</comments><wfw:commentRss>https://community.element14.com/challenges-projects/design-challenges/smart-home-healthcare-challenge/f/forum/57389/vitalsense-part-9-maxrefdes117-json-data-acquisition-via-usb-cdc-in-labview/rss?ContentTypeId=0</wfw:commentRss><description>&lt;p&gt;Excellent walkthrough on the clean setup!&lt;/p&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;</description></item></channel></rss>