[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/245/original/IMG_20260202_155008_561_2.jpg?1770047573","metadata":{}},{"element_type":"text","content":"\n  \n        \u003cp\u003e\u0026nbsp;Getting started is easy! You hold the \u003cstrong\u003eboot\u003c/strong\u003e button down on the back of the board while tapping the\u0026nbsp;\u003cstrong\u003ereset\u003c/strong\u003e button next to it, then afterwards let go. A new USB Drive will then show up on your computer, usually called\u0026nbsp;\u003ccode\u003eRP2\u003c/code\u003e or \u003ccode\u003eRP2350\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;Download the UF2 installation file \u003ca href=\"https://circuitpython.org/board/pimoroni_explorer2350/\" target=\"_blank\"\u003efrom the CircuitPython website\u003c/a\u003e for the Pimoroni® Explorer, or \u003ca href=\"https://github.com/adafruit/circuitpython/actions/runs/21594471324#:~:text=pimoroni_explorer2350\" target=\"_blank\"\u003efrom the pull-request on GitHub\u003c/a\u003e (requires a free account to access Actions artifacts), then copy the file with the suffix \u003ccode\u003e.uf2\u003c/code\u003e\u0026nbsp;over to the USB Drive, using Copy then Paste, or dragging a dropping the file onto the USB drive.\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;When it finishes copying over then the device reboots, and CircuitPython should load up after a few seconds (it sets up the filesystem for you), and then it presents a couple of new USB devices to your computer:\u003cbr\u003e\u003cbr\u003eThe \u003cstrong\u003eREPL\u003c/strong\u003e (\u003cem\u003eRead Evaluate Print Loop\u003c/em\u003e - an interactive circuitpython terminal) becomes available on a USB Serial Port (COM ports on Windows), and the \u003cstrong\u003euser filesystem\u003c/strong\u003e (where the code lives) as a USB \u003cem\u003eMass Storage device\u003c/em\u003e (USB Drive) called CIRCUITPY.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;The main drive you'll use is the \u003ccode\u003eCIRCUITPY\u003c/code\u003e drive, which is where the user code (\u003ccode\u003e/code.py\u003c/code\u003e), extra software libraries (in \u003ccode\u003e/lib\u003c/code\u003e or the root folder \u003ccode\u003e/\u003c/code\u003e), and any assets like images or mp3 files reside.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u0026nbsp;There can be an extra drive in CircuitPython version 10 for game \u003ccode\u003eSAVES\u003c/code\u003e (optional), and sometimes also for \u003ccode\u003eSD\u003c/code\u003e which is when an SD card is attached to your circuitpython board.\u0026nbsp;\u003c/em\u003e\u003c/p\u003e\n      \n","metadata":{}},{"element_type":"text","content":"\n  \n  \n        \u003ch2\u003eTest the basic functionality:\u003c/h2\u003e\n\u003cp\u003eIn the next few steps you will \u003cstrong\u003efirst \u003c/strong\u003ecopy the \u003ccode\u003ecode.py\u003c/code\u003e file over to your board, which is \u003cspan style=\"text-decoration: underline;\"\u003eat the end of these instructions\u003c/span\u003e, and you'll need the required software libraries too (which is the hardest part).\u003c/p\u003e\n\u003cp\u003eObtaining the required libraries can be done a few ways, either:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003ca href=\"https://circuitpython.org/libraries\" target=\"_blank\"\u003eDownload the library bundle from circuitpython.org\u003c/a\u003e and copy over the libraries you need to CIRCUITPY.\u003c/li\u003e\n\u003cli\u003eUse a tool like \u003cstrong\u003e\u003ca href=\"https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup\" target=\"_blank\"\u003ecircup\u003c/a\u003e\u003c/strong\u003e to automatically install the needed libraries (\u003cstrong\u003eit's amazing\u003c/strong\u003e, reads code.py file)\u003c/li\u003e\n\u003cli\u003eUse a web-based editor that can install libraries (**\u003cstrong\u003eeasiest \u003c/strong\u003eif you're new as no setup required)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eSo, you'll want to know about all three methods, probably learn about the second (\u003ccode\u003ecircup\u003c/code\u003e) as it's capable of installing libraries in pure python format (easily readable and editable) plus examples and working remotely over wifi too.\u003c/p\u003e\n\u003cp\u003eUsing the library bundle zips is foolproof (and has python or mpy versions), but for now we'll look at the 3rd, easiest method.\u003c/p\u003e\n\u003cp\u003eAll of the libraries have an associated GitHub repository, where you can read any documentation, see examples, read the code, and contribute any improvements that you make (called a Pull Request).\u003c/p\u003e\n\u003ch4\u003eGoto this website, and connect the USB drive to the website:\u003cbr\u003e\u003cem\u003e(make sure you select the CIRCUITPY drive when asked and not some other location)\u003c/em\u003e\n\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://urfdvw.github.io/circuitpython-online-ide-2/\" target=\"_blank\"\u003e\u003cstrong\u003ehttps://urfdvw.github.io/circuitpython-online-ide-2/\u003c/strong\u003e\u003c/a\u003e\u003c/p\u003e\n      \n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/242/original/Screenshot_2026-02-02_152106.png?1770045820","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        \u003cp\u003eYou'll be asked to ALLOW access to the drive you selected, accept to continue, deny to cancel.\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/243/original/Screenshot_2026-02-02_152427.png?1770045925","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        \u003cp\u003eThen the intreface shows the files on your Pimoroni / Circuitpython device.\u003c/p\u003e\n\u003cp\u003eClick on the code.py file, and replace it's contents with the code.py example below.\u003c/p\u003e\n\u003cp\u003eUse the TOOLS menu to access the Library Management.\u003c/p\u003e\n\u003cp\u003eIt will need to download a few files (the bundles of libraries in different formats) before you can continue further, which takes a minute or so the first time, then the Auto Install option becomes available (use it).\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","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/244/original/Screenshot_2026-02-02_153040.png?1770056676","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        \u003cp\u003eAfter installing the libraries (or updating any file) the CircuitPython board reloads the code.py file and execution begins again (or it tries to at least). Once all needed libraries are installed the code will continue to run and you'll see a menu on the display! Press the X button to change page.\u003c/p\u003e\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        \u003ch2\u003eCode.py file: \u003cem\u003e(save as code.py on your CIRCUITPY drive)\u003c/em\u003e\n\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":"code","content":"# SPDX-FileCopyrightText: 2026 Pimoroni Explorer 2350 Demo, authored by Tyeth Gundry + Claude/Gemini, for Adafruit Industries.\n# SPDX-License-Identifier: MIT\n\n\"\"\"\nPimoroni Explorer 2350 Feature Demo\n\nThis demo showcases all the features of the Explorer board:\n- 2.8\" IPS LCD display (320x240, ST7789V)\n- 6 tactile buttons (A, B, C, X, Y, Z) + User button\n- Piezo speaker with amplifier\n- 4 servo outputs\n- 6 ADC inputs (A0-A5)\n- Status LED\n- I2C via Qw/ST connectors\n\nMulti-Sensor Stick sensors (if connected):\n- BME280: Temperature, humidity, pressure\n- LTR-559: Light and proximity\n- ICM20948: 9-DOF IMU (accelerometer, gyroscope, magnetometer)\n\nControls:\n- Button A: Toggle LED\n- Button B: Play tone\n- Button X: Cycle display pages\n- Button Y: Move servo\n- Button Z: Reset servo\n- Button C: Toggle auto-refresh\n- User Button: I2C scan\n\"\"\"\n\nimport time\nimport board\nimport digitalio\nimport pwmio\nimport analogio\nimport displayio\nimport terminalio\nfrom adafruit_display_text import label\nfrom adafruit_display_shapes.rect import Rect\nfrom adafruit_display_shapes.circle import Circle\n\n# =============================================================================\n# Hardware Configuration\n# =============================================================================\n\n# Buttons (directly on board - directly read, active LOW, directly pull-up)\nBUTTON_PINS = {\n    \"A\": board.SW_A,\n    \"B\": board.SW_B,\n    \"C\": board.SW_C,\n    \"X\": board.SW_X,\n    \"Y\": board.SW_Y,\n    \"Z\": board.SW_Z,\n    \"USER\": board.SW_USER,\n}\n\n# Servo pins\nSERVO_PINS = [board.SERVO1, board.SERVO2, board.SERVO3, board.SERVO4]\n\n# ADC pins\nADC_PINS = [board.A0, board.A1, board.A2, board.A3, board.A4, board.A5]\n\n# Audio\nAUDIO_PIN = board.AUDIO\nAMP_EN_PIN = board.AMP_EN\n\n# LED\nLED_PIN = board.LED\n\n# I2C addresses for Multi-Sensor Stick\nBME280_ADDR = 0x76  # or 0x77\nBME280_ADDR_ALT = 0x77\nLTR559_ADDR = 0x23\nLSM6DS3_ADDR = 0x6A\nLSM6DS3_ADDR_ALT = 0x6B\n\n\n# =============================================================================\n# Setup Hardware\n# =============================================================================\n\n# Get the built-in display\ndisplay = board.DISPLAY\n\n# Setup LED (active-high - standard LED, True=ON, False=OFF)\nled = digitalio.DigitalInOut(LED_PIN)\nled.direction = digitalio.Direction.OUTPUT\nled.value = False  # Start with LED off\n\n# Setup buttons with pull-up (active low)\n# Note: USER button has external pull-up on board, don't configure pull\nbuttons = {}\nfor name, pin in BUTTON_PINS.items():\n    btn = digitalio.DigitalInOut(pin)\n    btn.direction = digitalio.Direction.INPUT\n    if name != \"USER\":\n        btn.pull = digitalio.Pull.UP\n    # USER button has external pull, leave it as-is\n    buttons[name] = btn\n\n# Setup amplifier enable\namp_en = digitalio.DigitalInOut(AMP_EN_PIN)\namp_en.direction = digitalio.Direction.OUTPUT\namp_en.value = False  # Start with amp off\n\n# Setup audio PWM (for simple tones)\naudio_pwm = None  # Will be created when needed\n\n# Setup servo PWM on first servo pin (for demo)\nservo_pwm = pwmio.PWMOut(SERVO_PINS[0], frequency=50, duty_cycle=0)\n\n# Setup ADC inputs\nadcs = []\nfor pin in ADC_PINS:\n    try:\n        adc = analogio.AnalogIn(pin)\n        adcs.append(adc)\n    except Exception:\n        adcs.append(None)\n\n# Setup I2C\ni2c = board.I2C()\n\n# =============================================================================\n# Sensor Classes (Driver Wrappers)\n# =============================================================================\n\nclass BME280Wrapper:\n    \"\"\"Wrapper for Adafruit BME280 driver.\n    \n    Follows usage from:\n    https://github.com/adafruit/Adafruit_CircuitPython_BME280/blob/main/examples/bme280_simpletest.py\n    \"\"\"\n    \n    def __init__(self, i2c, address=0x76):\n        self._sensor = None\n        try:\n            from adafruit_bme280 import basic as adafruit_bme280\n        except ImportError as e:\n            print(f\"BME280 Import Error: {e}\")\n            try:\n                import adafruit_bme280\n            except ImportError as e2:\n                print(f\"BME280 Fallback Import Error: {e2}\")\n                adafruit_bme280 = None\n        \n        if adafruit_bme280:\n            try:\n                if hasattr(adafruit_bme280, \"Adafruit_BME280_I2C\"):\n                    self._sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=address)\n                else:\n                    print(\"BME280: Adafruit_BME280_I2C class not found\")\n            except Exception as e:\n                print(f\"BME280 Init Error at 0x{address:x}: {e}\")\n                self._sensor = None\n    \n    @property\n    def available(self):\n        return self._sensor is not None\n    \n    def read(self):\n        if not self.available:\n            return None\n        try:\n            return (\n                self._sensor.temperature,\n                self._sensor.relative_humidity,\n                self._sensor.pressure\n            )\n        except Exception as e:\n            print(f\"BME280 Read Error: {e}\")\n            return None\n\n\nclass LTR559Wrapper:\n    \"\"\"Wrapper for Adafruit LTR559 driver.\"\"\"\n    \n    def __init__(self, i2c, address=0x23):\n        self._sensor = None\n        try:\n            from pimoroni_circuitpython_ltr559 import Pimoroni_LTR559\n        except ImportError as e:\n            print(f\"LTR559 Import Error: {e}\")\n            Pimoroni_LTR559 = None\n\n        if Pimoroni_LTR559:\n            try:\n                self._sensor = Pimoroni_LTR559(i2c)\n            except Exception as e:\n                print(f\"LTR559 Init Error: {e}\")\n                self._sensor = None\n    \n    @property\n    def available(self):\n        return self._sensor is not None\n    \n    def read(self):\n        if not self.available:\n            return None\n        try:\n            # Check attribute names\n            lux = self._sensor.get_lux() if hasattr(self._sensor, \"get_lux\") else (self._sensor.lux if hasattr(self._sensor, \"lux\") else self._sensor.light)\n            prox = self._sensor.get_proximity() if hasattr(self._sensor, \"get_proximity\") else self._sensor.proximity\n            return (lux, prox)\n        except Exception as e:\n            print(f\"LTR559 Read Error: {e}\")\n            return None\n\n\nclass LSM6DS3Wrapper:\n    \"\"\"Wrapper for Adafruit LSM6DS3 driver.\n    \n    Follows usage from:\n    https://github.com/adafruit/Adafruit_CircuitPython_LSM6DS/blob/main/examples/lsm6ds_lsm6ds3_simpletest.py\n    \"\"\"\n    \n    def __init__(self, i2c, address=0x6A):\n        self._sensor = None\n        try:\n            from adafruit_lsm6ds.lsm6ds3 import LSM6DS3\n        except ImportError as e:\n            print(f\"LSM6DS3 Import Error: {e}\")\n            LSM6DS3 = None\n            \n        if LSM6DS3:\n            try:\n                self._sensor = LSM6DS3(i2c, address=address)\n            except Exception as e:\n                print(f\"LSM6DS3 Init Error at 0x{address:x}: {e}\")\n                self._sensor = None\n    \n    @property\n    def available(self):\n        return self._sensor is not None\n    \n    def read_accel(self):\n        if not self.available:\n            return None\n        try:\n            return self._sensor.acceleration\n        except Exception as e:\n            print(f\"LSM6DS3 Read Error: {e}\")\n            return None\n\n\n\n# =============================================================================\n# Display UI\n# =============================================================================\n\n# Color palette\nBLACK = 0x000000\nWHITE = 0xFFFFFF\nRED = 0xFF0000\nGREEN = 0x00FF00\nBLUE = 0x0000FF\nYELLOW = 0xFFFF00\nCYAN = 0x00FFFF\nMAGENTA = 0xFF00FF\nORANGE = 0xFF8800\nGRAY = 0x888888\nDARK_GRAY = 0x444444\n\n# Display pages\nPAGE_MAIN = 0\nPAGE_SENSORS = 1\nPAGE_ADC = 2\nPAGE_BUTTONS = 3\nNUM_PAGES = 4\nPAGE_NAMES = [\"Main\", \"Sensors\", \"ADC\", \"Buttons\"]\n\ncurrent_page = PAGE_MAIN\n\ndef create_main_page():\n    \"\"\"Create the main status page.\"\"\"\n    group = displayio.Group()\n    \n    # Background\n    bg = Rect(0, 0, 320, 240, fill=DARK_GRAY)\n    group.append(bg)\n    \n    # Title\n    title = label.Label(\n        terminalio.FONT,\n        text=\"Pimoroni Explorer 2350\",\n        color=CYAN,\n        x=30,\n        y=15,\n        scale=2\n    )\n    group.append(title)\n    \n    # Subtitle\n    subtitle = label.Label(\n        terminalio.FONT,\n        text=\"CircuitPython Demo\",\n        color=WHITE,\n        x=95,\n        y=40\n    )\n    group.append(subtitle)\n    \n    # Status area\n    status_label = label.Label(\n        terminalio.FONT,\n        text=\"LED: OFF\",\n        color=GREEN,\n        x=10,\n        y=70\n    )\n    group.append(status_label)\n    \n    servo_label = label.Label(\n        terminalio.FONT,\n        text=\"Servo: 90°\",\n        color=YELLOW,\n        x=10,\n        y=90\n    )\n    group.append(servo_label)\n    \n    # Instructions\n    instructions = [\n        \"A: Toggle LED    B: Play tone\",\n        \"X: Next page     Y: Move servo\",\n        \"Z: Reset servo   C: Auto-refresh\",\n        \"USER: I2C scan\",\n    ]\n    for i, text in enumerate(instructions):\n        lbl = label.Label(\n            terminalio.FONT,\n            text=text,\n            color=WHITE,\n            x=10,\n            y=130 + i * 20\n        )\n        group.append(lbl)\n    \n    # Page indicator\n    page_label = label.Label(\n        terminalio.FONT,\n        text=f\"Page 1/{NUM_PAGES}: Main\",\n        color=ORANGE,\n        x=10,\n        y=225\n    )\n    group.append(page_label)\n    \n    return group, {\n        \"status\": status_label,\n        \"servo\": servo_label,\n        \"page\": page_label,\n    }\n\n\ndef create_sensors_page():\n    \"\"\"Create the sensors page for Multi-Sensor Stick.\"\"\"\n    group = displayio.Group()\n    \n    # Background\n    bg = Rect(0, 0, 320, 240, fill=DARK_GRAY)\n    group.append(bg)\n    \n    # Title\n    title = label.Label(\n        terminalio.FONT,\n        text=\"Multi-Sensor Stick\",\n        color=CYAN,\n        x=70,\n        y=15,\n        scale=2\n    )\n    group.append(title)\n    \n    # BME280 section\n    bme_title = label.Label(terminalio.FONT, text=\"BME280:\", color=GREEN, x=10, y=50)\n    group.append(bme_title)\n    \n    temp_label = label.Label(terminalio.FONT, text=\"Temp: --.-°C\", color=WHITE, x=20, y=70)\n    group.append(temp_label)\n    \n    humid_label = label.Label(terminalio.FONT, text=\"Humid: --.-%\", color=WHITE, x=20, y=85)\n    group.append(humid_label)\n    \n    press_label = label.Label(terminalio.FONT, text=\"Press: ----.-- hPa\", color=WHITE, x=20, y=100)\n    group.append(press_label)\n    \n    # LTR559 section\n    ltr_title = label.Label(terminalio.FONT, text=\"LTR-559:\", color=GREEN, x=170, y=50)\n    group.append(ltr_title)\n    \n    light_label = label.Label(terminalio.FONT, text=\"Light: ---- lux\", color=WHITE, x=180, y=70)\n    group.append(light_label)\n    \n    prox_label = label.Label(terminalio.FONT, text=\"Prox: ----\", color=WHITE, x=180, y=85)\n    group.append(prox_label)\n    \n    # ICM20948 section\n    imu_title = label.Label(terminalio.FONT, text=\"LSM6DS3 Accel:\", color=GREEN, x=10, y=130)\n    group.append(imu_title)\n    \n    accel_x = label.Label(terminalio.FONT, text=\"X: --.-- m/s²\", color=WHITE, x=20, y=150)\n    group.append(accel_x)\n    \n    accel_y = label.Label(terminalio.FONT, text=\"Y: --.-- m/s²\", color=WHITE, x=20, y=165)\n    group.append(accel_y)\n    \n    accel_z = label.Label(terminalio.FONT, text=\"Z: --.-- m/s²\", color=WHITE, x=20, y=180)\n    group.append(accel_z)\n    \n    # I2C status\n    i2c_status = label.Label(terminalio.FONT, text=\"I2C: Scanning...\", color=YELLOW, x=10, y=205)\n    group.append(i2c_status)\n    \n    # Page indicator\n    page_label = label.Label(\n        terminalio.FONT,\n        text=f\"Page 2/{NUM_PAGES}: Sensors\",\n        color=ORANGE,\n        x=10,\n        y=225\n    )\n    group.append(page_label)\n    \n    return group, {\n        \"temp\": temp_label,\n        \"humid\": humid_label,\n        \"press\": press_label,\n        \"light\": light_label,\n        \"prox\": prox_label,\n        \"accel_x\": accel_x,\n        \"accel_y\": accel_y,\n        \"accel_z\": accel_z,\n        \"i2c_status\": i2c_status,\n        \"page\": page_label,\n    }\n\n\ndef create_adc_page():\n    \"\"\"Create the ADC readings page.\"\"\"\n    group = displayio.Group()\n    \n    # Background\n    bg = Rect(0, 0, 320, 240, fill=DARK_GRAY)\n    group.append(bg)\n    \n    # Title\n    title = label.Label(\n        terminalio.FONT,\n        text=\"ADC Inputs\",\n        color=CYAN,\n        x=100,\n        y=15,\n        scale=2\n    )\n    group.append(title)\n    \n    # ADC labels and bars\n    adc_labels = []\n    adc_bars = []\n    colors = [RED, ORANGE, YELLOW, GREEN, CYAN, BLUE]\n    \n    for i in range(6):\n        y_pos = 50 + i * 28\n        \n        # Label\n        lbl = label.Label(\n            terminalio.FONT,\n            text=f\"A{i}: 0.00V\",\n            color=WHITE,\n            x=10,\n            y=y_pos\n        )\n        group.append(lbl)\n        adc_labels.append(lbl)\n        \n        # Bar background\n        bar_bg = Rect(100, y_pos - 6, 200, 12, fill=BLACK)\n        group.append(bar_bg)\n        \n        # Bar foreground (value indicator)\n        bar = Rect(100, y_pos - 6, 1, 12, fill=colors[i])\n        group.append(bar)\n        adc_bars.append(bar)\n    \n    # Page indicator\n    page_label = label.Label(\n        terminalio.FONT,\n        text=f\"Page 3/{NUM_PAGES}: ADC\",\n        color=ORANGE,\n        x=10,\n        y=225\n    )\n    group.append(page_label)\n    \n    return group, {\n        \"adc_labels\": adc_labels,\n        \"adc_bars\": adc_bars,\n        \"page\": page_label,\n    }\n\n\ndef create_buttons_page():\n    \"\"\"Create the button status page.\"\"\"\n    group = displayio.Group()\n    \n    # Background\n    bg = Rect(0, 0, 320, 240, fill=DARK_GRAY)\n    group.append(bg)\n    \n    # Title\n    title = label.Label(\n        terminalio.FONT,\n        text=\"Button Status\",\n        color=CYAN,\n        x=85,\n        y=15,\n        scale=2\n    )\n    group.append(title)\n    \n    # Button indicators\n    button_circles = {}\n    button_labels = {}\n    \n    # Layout: A B C on top row, X Y Z on bottom row, USER in center\n    positions = {\n        \"A\": (50, 80),\n        \"B\": (160, 80),\n        \"C\": (270, 80),\n        \"X\": (50, 140),\n        \"Y\": (160, 140),\n        \"Z\": (270, 140),\n        \"USER\": (160, 200),\n    }\n    \n    for name, (x, y) in positions.items():\n        # Circle indicator\n        circle = Circle(x, y, 20, fill=GRAY, outline=WHITE)\n        group.append(circle)\n        button_circles[name] = circle\n        \n        # Label\n        lbl = label.Label(\n            terminalio.FONT,\n            text=name,\n            color=WHITE,\n            x=x - 8 if len(name) == 1 else x - 16,\n            y=y + 30\n        )\n        group.append(lbl)\n        button_labels[name] = lbl\n    \n    # Page indicator\n    page_label = label.Label(\n        terminalio.FONT,\n        text=f\"Page 4/{NUM_PAGES}: Buttons\",\n        color=ORANGE,\n        x=10,\n        y=225\n    )\n    group.append(page_label)\n    \n    return group, {\n        \"circles\": button_circles,\n        \"labels\": button_labels,\n        \"page\": page_label,\n    }\n\n\n# =============================================================================\n# Audio Functions\n# =============================================================================\n\ndef play_tone(frequency, duration=0.1):\n    \"\"\"Play a tone through the piezo speaker.\"\"\"\n    global audio_pwm\n    \n    # Enable amplifier\n    amp_en.value = True\n    \n    # Create PWM for audio if needed\n    if audio_pwm is None:\n        audio_pwm = pwmio.PWMOut(AUDIO_PIN, frequency=frequency, duty_cycle=0, variable_frequency=True)\n    else:\n        audio_pwm.frequency = frequency\n    \n    # Play tone (50% duty cycle)\n    audio_pwm.duty_cycle = 32768\n    time.sleep(duration)\n    audio_pwm.duty_cycle = 0\n    \n    # Disable amplifier\n    amp_en.value = False\n\n\ndef play_melody():\n    \"\"\"Play a simple melody.\"\"\"\n    # Simple scale\n    notes = [262, 294, 330, 349, 392, 440, 494, 523]  # C4 to C5\n    for note in notes:\n        play_tone(note, 0.1)\n        time.sleep(0.05)\n\n\n# =============================================================================\n# Servo Functions\n# =============================================================================\n\nservo_angle = 90\n\ndef set_servo_angle(angle):\n    \"\"\"Set servo to a specific angle (0-180 degrees).\"\"\"\n    global servo_angle\n    servo_angle = max(0, min(180, angle))\n    \n    # Convert angle to duty cycle\n    # Typical servo: 1ms (0°) to 2ms (180°) pulse at 50Hz\n    # Duty cycle range: ~3276 (2.5%) to ~8192 (6.25%) at 16-bit\n    min_duty = 1638   # ~2.5% = 0.5ms\n    max_duty = 8192   # ~12.5% = 2.5ms\n    duty_range = max_duty - min_duty\n    \n    duty = int(min_duty + (duty_range * servo_angle / 180))\n    servo_pwm.duty_cycle = duty\n\n\n# =============================================================================\n# I2C Functions\n# =============================================================================\n\ndef scan_i2c():\n    \"\"\"Scan I2C bus and return list of found addresses.\"\"\"\n    while not i2c.try_lock():\n        pass\n    try:\n        devices = i2c.scan()\n        return devices\n    finally:\n        i2c.unlock()\n\n\n# =============================================================================\n# Main Application\n# =============================================================================\n\ndef main():\n    global current_page\n    \n    print(\"Pimoroni Explorer 2350 Demo\")\n    print(\"=\" * 40)\n    \n    # Initialize sensors (use driver-backed wrappers when available)\n    bme280 = BME280Wrapper(i2c, BME280_ADDR)\n    if bme280._sensor is None or not bme280.available:\n        bme280 = BME280Wrapper(i2c, BME280_ADDR_ALT)\n    ltr559 = LTR559Wrapper(i2c, LTR559_ADDR)\n    lsm6ds3 = LSM6DS3Wrapper(i2c, LSM6DS3_ADDR)\n    if lsm6ds3._sensor is None or not lsm6ds3.available:\n        lsm6ds3 = LSM6DS3Wrapper(i2c, LSM6DS3_ADDR_ALT)\n\n    \n    # Initial I2C scan\n    print(\"Scanning I2C bus...\")\n    devices = scan_i2c()\n    print(f\"Found {len(devices)} devices: {[hex(d) for d in devices]}\")\n    \n    # Create display pages\n    pages = [\n        create_main_page(),\n        create_sensors_page(),\n        create_adc_page(),\n        create_buttons_page(),\n    ]\n    \n    # Set initial page\n    display.root_group = pages[current_page][0]\n    \n    # Initialize servo to center\n    set_servo_angle(90)\n    \n    # Button debounce tracking\n    button_states = {name: True for name in buttons}  # True = not pressed (pull-up)\n    last_press_time = {name: 0 for name in buttons}\n    DEBOUNCE_TIME = 0.2\n    \n    # Auto-refresh toggle\n    auto_refresh = True\n    last_refresh = time.monotonic()\n    REFRESH_INTERVAL = 0.5\n    \n    print(\"Starting main loop...\")\n    print(\"Press buttons to interact!\")\n    \n    while True:\n        current_time = time.monotonic()\n        \n        # Read all buttons\n        for name, btn in buttons.items():\n            current_state = btn.value  # True = not pressed\n            prev_state = button_states[name]\n            \n            # Debug: print USER button state changes\n            if name == \"USER\" and current_state != prev_state:\n                print(f\"USER button: prev={prev_state}, curr={current_state}\")\n            \n            # Detect button press (transition from not pressed to pressed)\n            if prev_state and not current_state:  # Button just pressed\n                if current_time - last_press_time[name] \u003e DEBOUNCE_TIME:\n                    last_press_time[name] = current_time\n                    \n                    # Handle button press\n                    if name == \"A\":\n                        led.value = not led.value\n                        print(f\"LED: {'ON' if led.value else 'OFF'}\")\n                        play_tone(440, 0.05)\n                    \n                    elif name == \"B\":\n                        print(\"Playing melody...\")\n                        play_melody()\n                    \n                    elif name == \"C\":\n                        auto_refresh = not auto_refresh\n                        print(f\"Auto-refresh: {'ON' if auto_refresh else 'OFF'}\")\n                        play_tone(880 if auto_refresh else 220, 0.05)\n                    \n                    elif name == \"X\":\n                        current_page = (current_page + 1) % NUM_PAGES\n                        display.root_group = pages[current_page][0]\n                        print(f\"Page: {PAGE_NAMES[current_page]}\")\n                        play_tone(660, 0.05)\n                    \n                    elif name == \"Y\":\n                        new_angle = servo_angle + 30\n                        if new_angle \u003e 180:\n                            new_angle = 0\n                        set_servo_angle(new_angle)\n                        print(f\"Servo: {servo_angle}°\")\n                        play_tone(550, 0.05)\n                    \n                    elif name == \"Z\":\n                        set_servo_angle(90)\n                        print(\"Servo reset to 90°\")\n                        play_tone(330, 0.05)\n                    \n                    elif name == \"USER\":\n                        print(\"Scanning I2C...\")\n                        devices = scan_i2c()\n                        print(f\"Found: {[hex(d) for d in devices]}\")\n                        # Update sensors page I2C status if available\n                        if current_page == PAGE_SENSORS:\n                            pages[PAGE_SENSORS][1][\"i2c_status\"].text = f\"I2C: {len(devices)} @ {[hex(d) for d in devices][:3]}\"\n                        play_tone(770, 0.1)\n            \n            button_states[name] = current_state\n        \n        # Always update button page when on it (regardless of auto_refresh)\n        if current_page == PAGE_BUTTONS:\n            for name, btn in buttons.items():\n                pressed = not btn.value\n                pages[PAGE_BUTTONS][1][\"circles\"][name].fill = GREEN if pressed else GRAY\n        \n        # Update display if auto-refresh is on\n        if auto_refresh and (current_time - last_refresh \u003e REFRESH_INTERVAL):\n            last_refresh = current_time\n            labels = pages[current_page][1]\n            \n            if current_page == PAGE_MAIN:\n                labels[\"status\"].text = f\"LED: {'ON' if led.value else 'OFF'}\"\n                labels[\"servo\"].text = f\"Servo: {servo_angle}°\"\n                labels[\"page\"].text = f\"Page {current_page + 1}/{NUM_PAGES}: {PAGE_NAMES[current_page]}\"\n            \n            elif current_page == PAGE_SENSORS:\n                # Update BME280\n                if bme280.available:\n                    data = bme280.read()\n                    if data:\n                        labels[\"temp\"].text = f\"Temp: {data[0]:.1f}°C\"\n                        labels[\"humid\"].text = f\"Humid: {data[1]:.1f}%\"\n                        labels[\"press\"].text = f\"Press: {data[2]:.2f} hPa\"\n                else:\n                    labels[\"temp\"].text = \"Temp: N/A\"\n                    labels[\"humid\"].text = \"Humid: N/A\"\n                    labels[\"press\"].text = \"Press: N/A\"\n                \n                # Update LTR559\n                if ltr559.available:\n                    data = ltr559.read()\n                    if data:\n                        labels[\"light\"].text = f\"Light: {data[0]:.0f} lux\"\n                        labels[\"prox\"].text = f\"Prox: {data[1]}\"\n                else:\n                    labels[\"light\"].text = \"Light: N/A\"\n                    labels[\"prox\"].text = \"Prox: N/A\"\n                \n                # Update LSM6DS3\n                if lsm6ds3.available:\n                    accel = lsm6ds3.read_accel()\n                    if accel:\n                        labels[\"accel_x\"].text = f\"X: {accel[0]:+.2f} m/s²\"\n                        labels[\"accel_y\"].text = f\"Y: {accel[1]:+.2f} m/s²\"\n                        labels[\"accel_z\"].text = f\"Z: {accel[2]:+.2f} m/s²\"\n                else:\n                    labels[\"accel_x\"].text = \"X: N/A\"\n                    labels[\"accel_y\"].text = \"Y: N/A\"\n                    labels[\"accel_z\"].text = \"Z: N/A\"\n                \n                # I2C status\n                devices = scan_i2c()\n                i2c_label = f\"I2C: {len(devices)} devices: \"\n                for d in devices:\n                    i2c_label += f\"{hex(d)} \"\n                labels[\"i2c_status\"].text = i2c_label\n            \n            elif current_page == PAGE_ADC:\n                for i, adc in enumerate(adcs):\n                    if adc is not None:\n                        try:\n                            raw = adc.value\n                            voltage = raw * 3.3 / 65535\n                            labels[\"adc_labels\"][i].text = f\"A{i}: {voltage:.2f}V\"\n                            # Update bar width (0-200 pixels for 0-3.3V)\n                            bar_width = max(1, int(voltage / 3.3 * 200))\n                            labels[\"adc_bars\"][i].width = bar_width\n                        except Exception:\n                            labels[\"adc_labels\"][i].text = f\"A{i}: Error\"\n                    else:\n                        labels[\"adc_labels\"][i].text = f\"A{i}: N/A\"\n            \n            elif current_page == PAGE_BUTTONS:\n                for name, btn in buttons.items():\n                    pressed = not btn.value\n                    labels[\"circles\"][name].fill = GREEN if pressed else GRAY\n        \n        # Small delay to prevent CPU hogging\n        time.sleep(0.01)\n\n\n# Run the main application\nif __name__ == \"__main__\":\n    main()","metadata":{"language":"python","linenums":false,"filename":"code.py"}},{"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        \u003ch2\u003eGoing further and understanding CircuitPython (and code):\u003c/h2\u003e\n\u003cp\u003e\u003cspan style=\"text-decoration: underline;\"\u003eFollow the Getting Started link on the Circuitpython.org website!\u003c/span\u003e\u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp;I've linked to it here:\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","metadata":{}},{"element_type":"guide","content":"https://learn.adafruit.com/welcome-to-circuitpython/overview","metadata":{"guide_id":"1845","page_id":"11023"}},{"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        \u003cp\u003eThen, when you've begun to understand the madness, move on over to the CircuitPython Essentials, which has a few useful tips and tricks, and sections on each valuable new skill / feature that you'll want to play with, like how to play an MP3! (This board is not designed for high quality audio, more beeps and boops).\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","metadata":{}},{"element_type":"guide","content":"https://learn.adafruit.com/circuitpython-essentials/circuitpython-essentials","metadata":{"guide_id":"1954","page_id":"11673"}}]