[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/038/original/IMG_3034_2.jpeg?1763184179","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003ch2\u003eOverview\u003c/h2\u003e\n\u003cp\u003eA local production company is working on filming the first of a three-part sci-fi movie and needed a piece of scientific equipment for a laboratory scene. The executive producer/director found an obsolete flow cytometer analyzer in a government surplus sale, winning the bid for US$12. The device had the potential to look like a working DNA synthesizer with the addition of lighting and a bit of animation.\u003c/p\u003e\n\u003cp\u003eIn its day, the analyzer was a high-quality device that was robustly built to provide exceptional mechanical stability for its sensitive optical components. It was therefore quite heavy in spite of its size, requiring at least two persons to lift and position, which would increase the challenge to modify for use in the film. It was not a typical theatrical prop made from foam and balsa wood, for certain.\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003e\u003cspan class=\"Apple-converted-space\"\u003eI\u003c/span\u003e\u003c/span\u003e\u0026nbsp;was tasked with installing color lighting to enhance the device’s operational appearance for its brief appearance on-screen. To achieve this, I devised a plan to incorporate several \u003cem\u003eNeoPixel\u003c/em\u003e LED strips, which would be controlled by a \u003cstrong\u003e\u003cem\u003eCircuitPython\u003c/em\u003e\u003c/strong\u003e-based microcontroller, such as the \u003cem\u003eAdafruit\u003c/em\u003e\u0026nbsp;\u003cem\u003eM4 Express Feather\u003c/em\u003e. The multi-colored \u003cem\u003eNeoPixel\u003c/em\u003e LEDs could be strategically positioned both within and outside the device, thereby providing ambient illumination and symbolizing various functions, including sample loading and the incubation process.\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/040/original/Screenshot_2025-11-14_at_9.29.17%E2%80%AFPM.png?1763184690","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eGiven that the initial device employed industrial-grade servos (specifically, three \u003cem\u003eIMS MDI-17 Drive Plus Motion Control\u003c/em\u003e motors) for sample positioning and operating the sample fluid “sipper” needle, there was a preliminary aspiration to incorporate robotic physical movements beyond the lighting sequence. However, this objective was deferred due to the imminent project deadline, so a short puppetry cable would likely be attached to the sample positioning cam to animate movement of the test tube rack.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://youtu.be/2bkMd6YFgBg"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eProject Objective\u003c/h2\u003e\n\u003cp\u003eBuild a realistic theatrical prop for a science-fiction film. Animate an obsolete scientific laboratory device to make it appear fully functional and slightly ominous.\u003c/p\u003e\n\u003cp\u003e\u003cspan class=\"Apple-converted-space\"\u003e\u0026nbsp;\u003c/span\u003eRequirements:\u003c/p\u003e\n\u003col\u003e\n\u003col\u003e\n\u003cli\u003eInterior lighting with minor animation for sample processing\u003c/li\u003e\n\u003cli\u003eColored status lights and zone lighting changes to represent process states\u003c/li\u003e\n\u003cli\u003eDisguise original make/model with updated logos and graphics\u003c/li\u003e\n\u003cli\u003eTethered remote control of process lighting states\u003c/li\u003e\n\u003cli\u003eManual adjustment of zone brightness levels for location and camera sensitivity\u003c/li\u003e\n\u003cli\u003eSelf-contained lighting controller with no external circuitry or power supply\u003c/li\u003e\n\u003cli\u003eRemove unnecessary components and modules to reduce weight\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/ol\u003e\n\u003cp\u003eSecondary requirements (nice to have):\u003c/p\u003e\n\u003col\u003e\n\u003col\u003e\n\u003cli\u003eHide lighting strip pixels from direct camera view\u003c/li\u003e\n\u003cli\u003eAutomated sample chamber physical movement during loading\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/ol\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/041/original/IMG_2914_2.jpeg?1763184906","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eAnalyzer Lighting Design\u003c/h2\u003e\n\u003cp\u003eThe initial phase of the design process involved identifying the sections of the device that would benefit from illumination. The zones became the sample tube rack loading area near the fluid sipper, the processing area located just behind the sample loading area, the existing process stoplight indicator LEDs (red, yellow, green) mounted within the sample area, the sample tube rack uplight that would assist in illuminating test tube fluid, and the general interior of the device that was visible when the fluid container door was open. These lighting zones were subsequently designated as follows:\u003c/p\u003e\n\u003cul\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cspan\u003e\u003c/span\u003eMODE, Zone 0 (process stoplight indicators)\u003c/li\u003e\n\u003cli\u003e\n\u003cspan\u003e\u003c/span\u003eINT, Zone 1 (device interior)\u003c/li\u003e\n\u003cli\u003e\n\u003cspan\u003e\u003c/span\u003eCAVE, Zone 2 (sample processing area)\u003c/li\u003e\n\u003cli\u003e\n\u003cspan\u003e\u003c/span\u003eCELL, Zone 3 (sample tube rack loading area)\u003c/li\u003e\n\u003cli\u003e\n\u003cspan\u003e\u003c/span\u003eBEAM, Zone 4 (sample tube rack uplight)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/ul\u003e\n\u003cp\u003eFollowing the identification of the lighting zones, a temporary \u003cem\u003eNeoPixel\u003c/em\u003e strip was installed in each zone to assess the quality of illumination, select color schemes, and test camera angles. No permanent installations were made during this phase.\u003c/p\u003e\n\u003cp\u003eSubsequently, the analyzer underwent a thorough cleaning process, and all unnecessary modules and devices were removed from its interior to reduce bulk and weight. Only components that would not be visible on camera were removed, including the four-color laser light source, precision light bench, sensor data acquisition subsystem, laboratory air and waste subsystems including the fluid pump, and high-voltage power supplies. The existing fluid piping, sensors, power supply, servo control board, and internal minicomputer\u003cspan class=\"Apple-converted-space\"\u003e\u0026nbsp; \u003c/span\u003ewere retained for potential future modifications.\u003c/p\u003e\n\u003cp\u003eBy removing the superfluous items from the analyzer, its weight was significantly reduced, facilitating easier handling and enabling it to be lifted by a single individual. This reduction also provided ample space for the installation of electronic circuitry, wiring, a lighting power supply, and the \u003cem\u003eNeoPixel\u003c/em\u003e LED light strips. During this phase, the final light strip configuration was installed in each zone, utilizing 1/8” acrylic rods to support the strips in areas without existing supports.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/042/original/IMG_2955_2.jpeg?1763185381","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eStarting with the interior, two strips were positioned in the top and back sections of the instrument, and near the fluid container door. A few pixels of one strip would also be utilized to illuminate the sample process area. A shortened strip was mounted within the sample tube rack area, and a 32-pixel \u003cem\u003eNeoPixel FeatherWing\u003c/em\u003e was positioned beneath the sample tube rack area to function as the uplight illuminator, both attached with pressure sensitive tape. The three existing process stoplight LEDs were connected to a \u003cem\u003eNeoRGB Stemma\u003c/em\u003e breakout module for integration into the lighting strip\u003cspan class=\"Apple-converted-space\"\u003e\u0026nbsp; \u003c/span\u003enetwork.\u003c/p\u003e\n\u003cp\u003eThe lighting strip network comprises all \u003cem\u003eNeoPixel\u003c/em\u003e strips into a singular serial string of 122 sequential pixels, each capable of independent addressing to support a 24-bit color spectrum. To facilitate daisy-chained network connectivity, the 3-pin connector for each strip was replaced with a 4-pin \u003cem\u003eDuPont\u003c/em\u003e-style connector for the strip’s 5-volt power, ground, data input, and data output. The lighting control board distributes the pixel data from the MCU to each strip within the network and establishes a direct connection to the 5-volt, 10-ampere lighting power supply.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/043/original/IMG_3035.jpeg?1763185573","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe initial pixel in the network is the \u003cem\u003eNeoRGB Stemma\u003c/em\u003e breakout module, which is utilized for controlling the red, yellow, and green LEDs of the process stoplight. The breakout module is connected to a \u003cem\u003eFeatherWing Proto\u003c/em\u003e board, which is attached to the \u003cem\u003eFeather Tripler\u003c/em\u003e along with the MCU board. Beyond controlling the stoplight LEDs, the \u003cem\u003eStemma\u003c/em\u003e module receives the 3-volt logic data input signal from the MCU and provides a 5-volt logic output for the remaining \u003cem\u003eNeoPixel\u003c/em\u003e lighting strips and \u003cem\u003eNeoPixel FeatherWing\u003c/em\u003e.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/044/original/IMG_3039.jpeg?1763185654","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eLighting Control Panel and Remote Control\u003c/h2\u003e\n\u003cp\u003eThe lighting control panel was designed to manually control the brightness of each zone and initiate analyzer processing states such as sleep, sample load, and analysis states. The control panel is mounted on the rear of the analyzer, concealing the controls and indicators from the camera. Manual control of zone brightness is crucial because scene ambient lighting levels are uncertain prior to filming and may require adjustment based on conditions and the camera’s brightness and color response.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/045/original/IMG_2975.jpeg?1763185828","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe panel was designed to provide brightness control for the interior cell, cave, simple loading area, and sample up-light. An additional control was incorporated on the panel to select a color for the up-light, enabling the choice of a color to enhance the glow of the colored test tube fluid.\u003c/p\u003e\n\u003cp\u003eIn addition to the variable brightness controls, push button controls to initiate and terminate animation processes and reset the MCU were integrated into the rear panel design. The start push button switch was also wired to a quarter-inch TRS jack for the tethered remote control push button. The remote control push button was designed for the protagonist or a concealed operator to advance the processing state. A small cable was attached to a cam in the sample tube rack positioning system, allowing a concealed operator to manually animate rack movement during test tube loading.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/046/original/Screenshot_2025-11-14_at_9.51.45%E2%80%AFPM.png?1763186013","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003ePower to the analyzer lighting system is controlled by a power switch mounted on the rear panel. The switch connects the output of a 5-volt, 50-watt power supply to the lighting circuitry and MCU.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eSoftware Design\u003c/h2\u003e\n\u003cp\u003eThe primary requirement of the\u0026nbsp;\u003cem\u003eCircuitPython\u003c/em\u003e code is to individually control each separate zone of \u003cem\u003eNeoPixels\u003c/em\u003e in accordance with the parameters for each process state, while simultaneously monitoring the back panel controls and controlling lighting animations. To achieve this, three classes were developed. The first class, named \u003cstrong\u003e\u003cem\u003ePixels\u003c/em\u003e\u003c/strong\u003e, enumerates the \u003cem\u003eNeoPixel\u003c/em\u003e addresses for each zone. The second class, \u003cstrong\u003e\u003cem\u003eState\u003c/em\u003e\u003c/strong\u003e, defines the process states and contains a state parameter dictionary that describes the color and brightness characteristics for the zones, as well as a timeout trigger for each state. The third class, \u003cstrong\u003e\u003cem\u003eColor\u003c/em\u003e\u003c/strong\u003e, defines the colors and palettes used in each zone and instantiates five \u003cstrong\u003e\u003cem\u003ePaletteFader\u003c/em\u003e\u003c/strong\u003e instances to control zone brightness.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/047/original/physical_to_logical_pixel_mapping.png?1763186132","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eFollowing the definition of the classes, the code instantiates all the peripheral devices, including the rear panel controls and switches, the piezo speaker, and an optional \u003cem\u003eOLED Display FeatherWing\u003c/em\u003e (which can be stacked on top of the MCU board). The OLED display was utilized solely for troubleshooting purposes and was not incorporated into the final analyzer system. Finally, five instances of the \u003cstrong\u003e\u003cem\u003eRangeSlicer.Slicer\u003c/em\u003e\u003c/strong\u003e class are instantiated to map the ranges of the back panel’s variable controls. The \u003cstrong\u003e\u003cem\u003eSlicer\u003c/em\u003e\u003c/strong\u003e class maps the values of the analog controls to the normalized values employed for brightness control and color palette selection. The \u003cstrong\u003e\u003cem\u003eSlicer\u003c/em\u003e\u003c/strong\u003e class incorporates directional hysteresis to enhance output value stability by mitigating analog control noise.\u003c/p\u003e\n\u003cp\u003eWithin the code, synchronous functions include zone filling routines, the piezo tone generator, an RGB tuple to 24-bit hexadecimal value converter, and a sequential advancement function to change to the next process state. Asynchronous functions are utilized for reading and responding to changes in the back panel controls, monitoring state transitions, two lighting animation routines, and status reporting. The primary processing loop code resides in the asynchronous \u003cstrong\u003e\u003cem\u003emain\u003c/em\u003e\u003c/strong\u003e module, which identifies and executes the asynchronous functions.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eHints and Tricks\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eImplement the \u003cem\u003eNeoRGB Stemma\u003c/em\u003e breakout to enable precise LED control and, as the initial pixel of the network, provide a 5V voltage adjustment for the subsequent NeoPixel strips.\u003c/li\u003e\n\u003cli\u003eUtilize a \u003cem\u003ePiezo Driver Amp\u003c/em\u003e for the piezo speaker, particularly when it is integrated into a device enclosure.\u003c/li\u003e\n\u003cli\u003eEmploy 1/8” acrylic rods (\u003ca href=\"https://www.tapplastics.com/product/plastics/plastic_rods_tubes_shapes/acrylic_rods/147\"\u003e\u003cspan\u003ehttps://www.tapplastics.com/product/plastics/plastic_rods_tubes_shapes/acrylic_rods/147\u003c/span\u003e\u003c/a\u003e) to secure and suspend the \u003cem\u003eNeoPixel\u003c/em\u003e strips within the device.\u003c/li\u003e\n\u003cli\u003eConnect a four-pin \u003cem\u003eDuPont\u003c/em\u003e-style connector that incorporates \u003cstrong\u003e\u003cem\u003eDataIn\u003c/em\u003e\u003c/strong\u003e and \u003cstrong\u003e\u003cem\u003eDataOut\u003c/em\u003e\u003c/strong\u003e signals to each \u003cem\u003eNeoPixel\u003c/em\u003e strip to facilitate the daisy-chained lighting network.\u003c/li\u003e\n\u003cli\u003eThe control panel layout and graphics were created using the \u003ca href=\"https://learn.adafruit.com/build-a-custom-front-panel/design-the-graphics\" target=\"_blank\"\u003eMake the Sandwich Label\u003c/a\u003e technique.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eCircuitPython Code Listing\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios\n# SPDX-License-Identifier: MIT\n\"\"\"\nanalyzer/code.py\nver 1.0 2025-11-09 11:16PM\n\nA custom scientific laboratory equipment theatrical prop lighting controller\nbuilt into a stripped flow cytometry analyzer.\n\nFor the Adafruit M4 Express Feather and optional OLED display.\n\nCedarGrove PaletteFader and RangeSlicer are available from the Adafruit\nCircuitPython Community Bundle.\n\nNeoPixel strip configuration (daisy-chained)\nSTRIP      Description         Pixel Range Offset Length\n---------- ------------------- ----------- ------ ------\nMODE_LEDS, Stoplight LEDs,     [  0:  0],   0,     1\nSTRIP_A,   Equipment Interior, [  1: 30],   1,    30\nSTRIP_B,   processing area,    [ 31: 53],  31,    23\nBEAM,      cell uplight,       [ 54: 85],  54,    32\nSTRIP_D,   cell chamber,       [ 86:122],  86,    37\n\nLighting zone logical configuration\nZONE  Description     Pixel Index\n----- --------------- -----------------------\nMODE, Process LEDs    [  0:   0]\nINT,  Interior,       [  1:  30], [ 41:  53]\nCAVE, Sample Cave,    [ 31:  40]\nCELL, Sample Chamber, [ 86: 122]\nBEAM, Under Cell,     [ 54:  85]\n\"\"\"\n\nimport asyncio\nimport board\nimport pwmio\nimport time\nimport gc\nfrom analogio import AnalogIn\nfrom digitalio import DigitalInOut, Direction, Pull\nimport neopixel\nfrom i2cdisplaybus import I2CDisplayBus\nimport adafruit_displayio_sh1107\nimport displayio\nfrom cedargrove_palettefader.palettefader import PaletteFader\nfrom cedargrove_rangeslicer import Slicer\n\nclass Pixels:\n    # Creates Lists of pixels for each zone\n    MODE = [0]  # A single pixel\n    INT = [pix for pix in range(1, 30 + 1)] + [pix for pix in range(41, 53 + 1)]\n    CAVE = [pix for pix in range(31, 40 + 1)]\n    CELL = [pix for pix in range(86, 122 + 1)]\n    BEAM = [pix for pix in range(54, 85 + 1)]\n\nclass State:\n    \"\"\"\n    Analyzer process state definitions and parameters.\n    \"\"\"\n    # Analyzer Process States\n    TEST = \"TEST\"  # For setting brightness and color\n    REST = \"REST\"  # Darkened analyzer with heartbeat\n    WAKE = \"WAKE\"  # Warm the analyzer for operation\n    IDLE = \"IDLE\"  # Wait for sample\n    LOAD = \"LOAD\"  # Load the sample\n    SCAN = \"SCAN\"  # Analyze the sample\n\n    ALL = [TEST, REST, WAKE, IDLE, LOAD, SCAN]\n\n    \"\"\"\n    State Parameters Dictionary\n    Define each zone's [brightness, color index] and state timeout parameters\n      - The brightness value is the factor to apply to the zone's rear panel\n        control setting\n      - The color index value is the index of the zone's color palette\n      - When the beam color index is None, the color is set by the rear panel\n        beam color control setting\n      - The state timeout parameter is in seconds; None will hold until the\n        start button is pressed to change to the next state\n        \n      Dictionary state definition example:\n        state   mode      int       cave      cell      beam         timeout\n        ---- :  --------  --------  --------  --------  -----------  -------\n        TEST : [[1.0, 4], [1.0, 1], [1.0, 1], [1.0, 2], [1.0, None], 30],\n    \"\"\"\n    params = {\n        TEST: [[1.0, 4], [1.0, 1], [1.0, 1], [1.0, 2], [1.0, None], 30],\n        REST: [[1.0, 1], [1.0, 1], [0.5, 1], [0.0, 1], [0.0, 1], None],\n        WAKE: [[1.0, 1], [1.0, 1], [0.5, 1], [0.0, 1], [0.0, 1], 1],\n        IDLE: [[1.0, 3], [1.0, 1], [1.0, 1], [0.0, 1], [0.0, 1], 2],\n        LOAD: [[1.0, 2], [1.0, 1], [1.0, 5], [1.0, 5], [1.0, 3], None],\n        SCAN: [[1.0, 1], [1.0, 1], [1.0, 1], [1.0, 2], [1.0, 1], None],\n    }\n\nclass Color:\n    \"\"\"\n    Define color values, reference palettes, and brightness-controlled\n    palette (PaletteFader) objects.\n    \"\"\"\n    # Some pure colors\n    RED = 0xFF0000\n    GREEN = 0x00FF00\n    YELLOW = 0xFFFF00\n    BLUE = 0x0000FF\n    PURPLE = 0xFF00FF\n    BLACK = 0x000000\n    WHITE = 0xFFFFFF\n\n    # For RGB Stemma-connected Mode LEDs\n    LED_RED = 0x400000  # RED LED\n    LED_GRN = 0x004000  # GRN LED\n    LED_YEL = 0x0000FF  # YEL LED\n    LED_ALL = 0x4040FF  # all LEDs ON\n\n    ref_palette = displayio.Palette(6)\n    ref_palette = [BLACK, RED, BLUE, GREEN, PURPLE, WHITE]\n\n    mode_palette = displayio.Palette(5)\n    mode_palette = [BLACK, LED_RED, LED_GRN, LED_YEL, LED_ALL]\n\n    # Define the zone pixel PaletteFader palettes\n    all_colors = [\n        PaletteFader(mode_palette, 0.0, normalize=False),\n        PaletteFader(ref_palette, 0.0, normalize=False),\n        PaletteFader(ref_palette, 0.0, normalize=False),\n        PaletteFader(ref_palette, 0.0, normalize=False),\n        PaletteFader(ref_palette, 0.0, normalize=False),\n    ]\n\n# Instantiate OLED Display as REPL if available\ndisplayio.release_displays()\ntry:\n    i2c = board.I2C()  # uses board.SCL and board.SDA\n    display_bus = I2CDisplayBus(i2c, device_address=0x3C)\n    display = adafruit_displayio_sh1107.SH1107(display_bus, width=128, height=76)\nexcept RuntimeError as err:\n    print(\"WARNING: display not found:\", err)\n    print(\"Analyzer will operate without display.\")\n\n# Instantiate the piezo buzzer; pin D4\npiezo = pwmio.PWMOut(board.D4, duty_cycle=0, frequency=440, variable_frequency=True)\n\n# Instantiate the two control panel push buttons\npb_start = DigitalInOut(board.D0)  # RX\npb_start.direction = Direction.INPUT\npb_start.pull = Pull.UP\n\npb_stop = DigitalInOut(board.D1)  # TX\npb_stop.direction = Direction.INPUT\npb_stop.pull = Pull.UP\n\n# Instantiate the OLED start button (for testing)\ndisp_sw_a = DigitalInOut(board.D9)  # OLED button A\ndisp_sw_a.direction = Direction.INPUT\ndisp_sw_a.pull = Pull.UP\n\n# Instantiate the five analog inputs (A1 - A5)\nanalog_in = [\n    AnalogIn(board.A1),\n    AnalogIn(board.A2),\n    AnalogIn(board.A3),\n    AnalogIn(board.A4),\n    AnalogIn(board.A5),\n]\n\n# Instantiate the five RangeSlicer instances for control value mapping\ncontrol = [\n    Slicer(in_min=400, in_max=65000, out_min=0, out_max=1.0,\n           out_slice=1 / 100, hyst_factor=0.25,\n           out_integer=False),  # INT brightness\n    Slicer(in_min=400, in_max=65000, out_min=0, out_max=1.0,\n           out_slice=1 / 100, hyst_factor=0.25,\n           out_integer=False),  # CAVE brightness\n    Slicer(in_min=400, in_max=65000, out_min=0, out_max=1.0,\n           out_slice=1 / 100, hyst_factor=0.25,\n           out_integer=False),  # CELL brightness\n    Slicer(in_min=400, in_max=65000, out_min=0, out_max=1.0,\n           out_slice=1 / 100, hyst_factor=0.25,\n           out_integer=False),  # BEAM brightness\n    Slicer(in_min=400, in_max=65000, out_min=1, out_max=5,\n           out_slice=1, hyst_factor=0.25,\n           out_integer=True),  # BEAM color idx\n]\n\n# Initialize the control previous value history list\nknob_old = [\n    0.0,  # INT brightness\n    0.0,  # CAVE brightness\n    0.0,  # CELL brightness\n    0.0,  # BEAM brightness\n    0.0,  # BEAM color index\n]\n\n# Instantiate NeoPixel data output pins for pixel strips and on-board status\nstatus = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.05)\nstatus[0] = Color.YELLOW  # Startup\npixel_strip = neopixel.NeoPixel(board.D5, 123, brightness=1.0)\n\ndef fill_mode(brightness=0.0, color_idx=4):\n    # Mode: Front Panel Process Traffic Lights; default ALL ON; zone index = 0\n    status[0] = Color.PURPLE  # Busy\n    Color.all_colors[0].brightness = brightness\n    for pixel in Pixels.MODE:\n        pixel_strip[pixel] = Color.all_colors[0].palette[color_idx]\n    status[0] = Color.BLUE  # Normal\n\ndef fill_int(brightness=0.0, color_idx=1):\n    # Interior: Analyzer Interior; default color RED; zone index = 1\n    status[0] = Color.PURPLE  # Busy\n    Color.all_colors[1].brightness = brightness\n    for pixel in Pixels.INT:\n        pixel_strip[pixel] = Color.all_colors[1].palette[color_idx]\n    status[0] = Color.BLUE  # Normal\n\ndef fill_cave(brightness=0.0, color_idx=1):\n    # Cave: Sample Chamber Cave Interior; default color RED; zone index = 2\n    status[0] = Color.PURPLE  # Busy\n    Color.all_colors[2].brightness = brightness\n    for pixel in Pixels.CAVE:\n        pixel_strip[pixel] = Color.all_colors[2].palette[color_idx]\n    status[0] = Color.BLUE  # Normal\n\ndef fill_cell(brightness=0.0, color_idx=2):\n    # Cell: Sample Chamber Exterior; default color BLUE; zone index = 3\n    status[0] = Color.PURPLE  # Busy\n    Color.all_colors[3].brightness = brightness\n    for pixel in Pixels.CELL:\n        pixel_strip[pixel] = Color.all_colors[3].palette[color_idx]\n    status[0] = Color.BLUE  # Normal\n\ndef fill_beam(brightness=0.0, color_idx=1):\n    # Beam: Uplight brightness and color; default color RED; zone index = 4\n    status[0] = Color.PURPLE  # Busy\n    Color.all_colors[4].brightness = brightness\n    for pixel in Pixels.BEAM:\n        pixel_strip[pixel] = Color.all_colors[4].palette[color_idx]\n    status[0] = Color.BLUE  # Normal\n\ndef beep(frequency=440, duration=0.5):\n    # Beep the piezo buzzer\n    piezo.frequency = frequency\n    piezo.duty_cycle = int(65535 / 2)  # Beep tone on; 50% duty cycle\n    time.sleep(duration)\n    piezo.duty_cycle = 0  # Beep tone off\n\ndef rgb_tuple_to_hex(value):\n    # Convert an RGB color tuple to a 24-bit hex value\n    return (value[0] \u003c\u003c 16) | (value[1] \u003c\u003c 8) | value[2]\n\ndef advance_state():\n    # Advance to next state; loop to REST state rather than TEST\n    global STATE\n    # Advance STATE to next state; loop back to REST\n    state_index = State.ALL.index(STATE)\n    state_index += 1\n    if state_index \u003e= len(State.ALL):\n        state_index = 1  # loop to REST state\n    STATE = State.ALL[state_index]\n    print(f\"* new STATE {STATE}\")\n\nasync def read_zone_controls():\n    # Watch for state change; read zone controls and update pixels\n    global STATE, knob_old\n    beep(523, 0.25)  # C6; wake-up\n    state_timer = time.monotonic()  # Initialize state duration timer\n    new_state = True  # Initialize state changed flag\n    while True:\n        await asyncio.sleep(0.6)\n        # Check Start switch; used for state change\n        if not (pb_start.value and disp_sw_a.value):\n            status[0] = Color.YELLOW  # Waiting for input\n            beep(523, 0.25)  # C6\n            print(f\"* button old: {STATE}\")\n            while not (pb_start.value and disp_sw_a.value):\n                time.sleep(0.1)\n            beep(784, 0.25)  # G5\n            advance_state()\n            new_state = True  # State changed flag\n            state_timer = time.monotonic()  # Start the state timeout timer\n            status[0] = Color.BLUE  # Normal\n\n        # STATE duration timeouts; used for state change\n        if State.params[STATE][5] is not None:\n            if time.monotonic() - state_timer \u003e= State.params[STATE][5]:\n                print(f\"* timeout old: {STATE}\")\n                advance_state()\n                new_state = True  # State changed flag\n                state_timer = time.monotonic()  # Start the state timeout timer\n\n        # Read panel zone control knobs\n        #   INT bright, CAVE bright, CELL bright, BEAM bright,  BEAM color\n        knob_value = [\n            round(control[0].range_slicer(analog_in[0].value)[0], 2),\n            round(control[1].range_slicer(analog_in[1].value)[0], 2),\n            round(control[2].range_slicer(analog_in[2].value)[0], 2),\n            round(control[3].range_slicer(analog_in[3].value)[0], 2),\n            round(control[4].range_slicer(analog_in[4].value)[0], 2),\n        ]\n\n        # Update Zone Brightness and Color Values\n\n        # INT Zone Brightness; knob index = 0, zone index = 1\n        if (knob_value[0] != knob_old[0]) or new_state:\n            # Update when value or state has changed\n            knob_old[0] = knob_value[0]\n            bright_factor, color_idx = State.params[STATE][1]  # Get params\n            fill_int(knob_value[0] * bright_factor, color_idx)\n\n        # CAVE Zone Brightness; knob index = 1, zone index = 2\n        if (knob_value[1] != knob_old[1]) or new_state:\n            # Update when value or state has changed\n            knob_old[1] = knob_value[1]\n            bright_factor, color_idx = State.params[STATE][2]  # Get params\n            fill_cave(knob_value[1] * bright_factor, color_idx)\n\n        # CELL Zone Brightness; knob index = 2, zone index = 3\n        if (knob_value[2] != knob_old[2]) or new_state:\n            # Update when value or state has changed\n            knob_old[2] = knob_value[2]\n            bright_factor, color_idx = State.params[STATE][3]  # Get params\n            fill_cell(knob_value[2] * bright_factor, color_idx)\n\n        # BEAM Zone Color; knob index = 4, zone index = 4\n        if State.params[STATE][4][1] is None:\n            # BEAM zone color set by control and latest color value\n            if (knob_value[4] != knob_old[4]) or new_state:\n                # Update when value or state has changed\n                knob_old[4] = knob_value[4]\n                bright_factor, _ = State.params[STATE][4]  # Get params\n                fill_beam(knob_value[3] * bright_factor, knob_value[4])\n        else:\n            # BEAM zone color set by state parameter\n            if new_state:\n                bright_factor, color_idx = State.params[STATE][4]  # Get params\n                fill_beam(knob_old[3] * bright_factor, color_idx)\n\n        # BEAM Zone Brightness; knob index = 3, zone index = 4\n        if (knob_value[3] != knob_old[3]) or new_state:\n            # Update when value or state has changed\n            knob_old[3] = knob_value[3]\n            bright_factor, _ = State.params[STATE][4]  # Get params\n            if State.params[STATE][4][1] is None:\n                # BEAM color set by latest control value\n                color_idx = knob_old[4]  # BEAM color knob\n            else:\n                # BEAM color is set by state parameter\n                _, color_idx = State.params[STATE][4]  # Get params\n            fill_beam(knob_value[3] * bright_factor, color_idx)\n\n        new_state = False\n\nasync def animate_int(delay=1.0):\n    while True:\n        await asyncio.sleep(delay)\n        for pixel in range(43, 45 + 1):\n            if rgb_tuple_to_hex(pixel_strip[45]) == Color.all_colors[1].palette[1]:\n                pixel_strip[pixel] = Color.all_colors[1].palette[3]\n                pixel_strip[pixel + 3] = Color.all_colors[1].palette[1]\n            else:\n                pixel_strip[pixel] = Color.all_colors[1].palette[1]\n                pixel_strip[pixel + 3] = Color.all_colors[1].palette[3]\n\nasync def animate_mode(delay=1.5, steps=15):\n    global STATE\n    while True:\n        # Gradually brighten and dim a mode LED\n        for i in range(int(steps * 0.10), steps + 1):\n            Color.all_colors[0].brightness = 1.0 * (i / steps)\n            pixel_strip[0] = Color.all_colors[0].palette[State.params[STATE][0][1]]\n            await asyncio.sleep(delay / steps)\n        for i in range(steps, int(steps * 0.10), -1):\n            Color.all_colors[0].brightness = 1.0 * (i / steps)\n            pixel_strip[0] = Color.all_colors[0].palette[State.params[STATE][0][1]]\n            await asyncio.sleep(delay / steps)\n\nasync def animate_status():\n    # Report current state, memory usage, and collect garbage\n    global STATE\n    while True:\n        gc.collect()\n        print(f\"-- {STATE} {time.monotonic():0.0f} sec\")\n        print(f\"   mem_free: {gc.mem_free() / 1000:3.0f}kb\")\n        await asyncio.sleep(15)\n\nasync def main():\n    # Define the async tasks\n    animate_int_task = asyncio.create_task(animate_int())\n    read_ctrls_task = asyncio.create_task(read_zone_controls())\n    animate_status_task = asyncio.create_task(animate_status())\n    animate_mode_task = asyncio.create_task(animate_mode())\n    # Start the async tasks\n    await asyncio.gather(\n        read_ctrls_task,\n        animate_int_task,\n        animate_status_task,\n        animate_mode_task,\n    )\n    print(\"async main() done\")  # Should never reach this point\n\n# ### PRIMARY PROCESS ###\n#   Establish the start-up state\nSTATE = State.TEST\n#   Start the asynchronous task manager\nasyncio.run(main())\nprint(\"primary process done\")  # Should never reach this point","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eFeather M4 Express Directory Contents\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/048/original/Screenshot_2025-11-14_at_10.03.18%E2%80%AFPM.png?1763186625","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eHardware Block Diagram\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/049/original/hardware_block_diagram.png?1763186674","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eSchematic Diagram\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/121/original/schematic.png?1767239417","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eAdafruit Bill of Materials\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3857","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4801","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3919","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2945","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5888","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/571","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2884","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/658","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/562","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3306","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3892","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/3890","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5791","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/160","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4650","metadata":{}}]