# Jumperless V5 - Complete Documentation > This file contains the full text of all Jumperless V5 documentation, > intended for use by LLMs and AI assistants. > > Source: https://docs.jumperless.org > Generated from MkDocs source files. --- # Home --- ## What is it? Jumperless V5 lets you prototype like a nerdy wizard who can see electricity and conjure jumpers with a magic wand. It’s an Integrated Development Environment (IDE) for hardware, with an analog-by-nature RP2350B dev board, a drawer full of wires, and a workbench full of test equipment (including a power supply, a multimeter, an oscilloscope, a function generator, and a logic analyzer) all crammed inside a breadboard. You can connect any point to any other using software-defined jumpers, so the four individually programmable ±8 V power supplies; ten GPIO; and seven management channels for voltage, current, and resistance can all be connected anywhere on the breadboard or the Arduino Nano header. RGB LEDs under each hole turn the breadboard itself into a display that provides real-time information about whatever’s happening in your circuit. It's not just about being too lazy to plug in some jumpers. With software controlled wiring, the circuit *itself* is now [***scriptable***](08-micropython.md), which opens up a world of infinite crazy new things you could never do on a regular breadboard. Have a script try out every combination of parts until it does what you want (*à la* [evolvable hardware](https://evolvablehardware.org/)), automatically switch around audio effects on the fly, characterize some unknown chip with the part numbers sanded off, or don't bother with any of that and just [play Doom on it](https://www.youtube.com/watch?v=xWYWruUO0F4). But more likely, you'll be using it to get circuits from your brain into hardware with so little friction it feels like you're just thinking them into existence. So yeah, wizard shit. These are the docs where you will learn how to wield your new powers --- ## If you don't already have one ### [Get the new Jumperless V5 rev 7](https://shop.jumperless.org/products/jumperless-v5-rev-7) Or if you want to save some money and get a refurbished one, ### [Jumperless V5 offcuts](https://shop.jumperless.org/products/jumperless-v5) ### [Get a Jumperless V5 on Crowd Supply](https://www.crowdsupply.com/architeuthis-flux/jumperless-v5) ### [Preorder the ALASKAN BULL WORM! PSRAM Mod Kit](https://shop.jumperless.org/products/alaskan-bull-worm-jumperless-v5-psram-mod-kit) --- ## Getting Started [Image: guide-42] ## Documentation Sections - **[Basic Controls](01-basic-controls.md)** - Learn how to use the probe, click wheel, and slot system - **[The App](03-app.md)** - For talking to your Jumperless, importing from Wokwi, and flashing Arduino sketches - **[OLED](04-oled.md)** - Add a better display - **[Arduino](05-arduino.md)** - UART passthrough and automatic flashing - **[Configuration](06-config.md)** - Persistent settings - **[Debugging](07-debugging.md)** - Crossbar, bridge, and net list views - **[File Manager](08-file-manager.md)** - Filesystem access, YAML slot file editing, and text editor - **[MicroPython](08-micropython.md)** - Use the onboard MicroPython interpreter - **[MicroPython API Reference](09.5-micropythonAPIreference.md)** - All the Jumperless-specific hardware calls - **[Odds and Ends](09.8-odds-and-ends.md)** - Stuff I couldn't think of a good category for - **[3D Printable Stand](10-3d-stand.md)** - Print your own stand - **[Glossary](99-glossary.md)** - Key terms including slots, nodes, bridges, and the W command (You should turn off [Dark Reader](https://darkreader.org/) for this site if you have it, it messes up the sidebar colors) --- ## Find Me On The Internet --- See what Zack Freedman had to say about it: Apparently he didn't find this site, which makes sense, he was an early backer and at the time, the QR code on the box didn't take you here. --- Watch [Alex Glow](https://alexglow.com/) and [David Groom](https://exposed.ishotjr.com/) screw around with their Jumperlesses for a few hours: This also serves as a cautionary tale for what happens if you don't skim these docs first (which is a completely valid approach, you do you.) --- Me talking to [Ian Buckley](https://www.linkedin.com/in/ianmbuckley/) about Jumperless V5: The hardware was so rudimentary at this point that I would barely even call the thing I was showing there a "V5", it was revision 1 and a *lot* has been done since then. --- Chillin' with [Alex Lynd](https://alexlynd.com/) We're talking about an even earlier set of prototypes, this was shot at both Teardown 2024 and Hackaday Supercon 2024 so it's rev 1 and 2 shown here. --- The OG Jumperless video: Most of the stuff here is relevant as a subset of what Jumperless V5 does. --- [Image: dymo-6s] --- **For AI/LLM tools:** This documentation is available as [llms.txt](/llms.txt) (index) and [llms-full.txt](/llms-full.txt) (complete text) for easy ingestion. --- --- # Basic Controls [Image: guide-42] ---- Connect: Connect button Idle --> Remove: Remove button Connect --> Remove: Remove button Remove --> Connect: Connect button Connect --> Idle: Connect button Remove --> Idle: Remove button ``` ---- --> ## The Probe First, keep the switch on the probe set to `Select` [Image: ProbeSelect] ## Connecting Rows Click the `Connect` button on the probe [Image: connectButton] The logo should turn blue and the LEDs on the probe should also change [Image: connect] Now any pair of nodes you tap should get connected as you make them. In connect mode, you're creating `bridges` (see the [glossary](99-glossary.md)), so connections are made in pairs. When you've tapped the first `node` in a pair, the `logo` and `Connect` text on the probe will brighten to show that you're "`holding`" a connection, and the next thing you tap will connect to that first `node`. If you make a mistake while `holding` a connection, click the `Connect` button and it will clear it and take you back to the first `node`. If you click the `Connect` button while you're not `holding` a `node`, it will leave `probe mode` and bring you back into `idle mode` (rainbowy`logo`, all 3 `probe LED`s on.) To get out of `Connect` mode, press the button again. If you make a connection you didn't mean to, **double-tap `Remove`** to undo it - see [Undo / Redo](#undo-redo). ### Encoder Connections You can also make connections using just the clickwheel, without needing to touch the probe to the breadboard: **To activate:** - Navigate to: `Click` > `Connect` > `Add` (or `Remove`) - OR just turn the clickwheel while already in probe mode **How it works:** 1. Turn the clickwheel to scroll through all available nodes: - Breadboard rows (1-60) - Nano header pins (D0-A7) - Rails (Top, Bottom, GND) - DAC (0, 1) - ADC (0-4, Probe) - GPIO (1-8) - UART (TX, RX) - Current sense (I+, I-) 2. Click the encoder button to select the highlighted node 3. Hold the encoder button to exit The cursor will automatically hide after 5 seconds of inactivity. This is especially useful when you need precise control or want to access special functions without tapping pads. ## Removing Rows Click the `Remove` button [Image: removeButton] and the logo should turn reddish [Image: remove] Now you can swipe along the `pad`s or tap them one at a time. Remember it only disconnects that `node` and anything connected to it directly, not *everything* on the `net`. So tapping say, `row 25` that's connected to `GND` won't clear everything connected to `GND`, but tapping the `-` on the rails (for `GND`) would. The special functions work the same way, tap the pad, pick one, and it will remove it. Click the button again to get out. If you delete the wrong row, **double-tap `Remove`** to undo it (or use the History scrub menu to walk back further) - see [Undo / Redo](#undo-redo). ## Probe Notes **Remember the probe is read by a resistive voltage divider**, so putting your fingers on the pads (or the back sides of the 4 risers that connect those `probe sense` boards to the main board), or anything causing the probe tip not to be at a steady 3.3V will give you weird readings. If you can't seem to stop playing with the switch on the probe, run the app `probe calib` and tap around on the board while turning the clickwheel until the place you tapped is always spot on (do this with the switch in both modes), and hold the clickwheel button to save. This adjusts the nominal 3.3V `measure` mode puts out should be fairly accurate enough for probing. ## The Click Wheel [Image: wheel copy] There are two kinds of presses, `click` (short press) and `hold` (long press). In general, a `click` (short) is a `yes`, and a `hold` (long) is a `no`/`back`/`exit`/`whatever`. When I say `click`, it's more of a diagonal slide toward the center of the board ([these encoders](https://lcsc.com/product-detail/Rotary-Encoders_Mitsumi-Electric-SIQ-02FVS3_C2925423.html) were meant to poke out just a little bit from the side of a tablet or whatever.) To get to the menu, `click` the button and scroll through the menus, `click` will bring you into that menu, `hold` will take you back one level. If you have trouble reading stuff on the breadboard LEDs, everything is copied to the Serial terminal and the OLED (talked about in [OLED Section](04-oled.md)), and adjusting the brightness may help; in the menus, it's `Display Options` > `Bright` > `Menu` and then scroll around until you find a level you like, then `click` to confirm. ## Special Functions To connect to `special functions`, tap the corresponding `pad` near the logo, it will show you a menu on the breadboard and terminal to choose them. [Image: gpioTapped] You can think of `special functions` just like any other `node`, the only difference is they're in a sort of "folder" so I didn't need to put a dedicated pad for each of them. ```jython DAC Pad └─ 0 1 [Tap pads below selection]¹ └─ -8V !:.:! +8V [Tap bottom pads or use clickwheel to select a voltage] > [click probe Connect button to confirm]² └─ [Tap a row to connect it to] (or if you were already "holding" a node, it'll connect there)³ ``` [Image: This is what prints in the terminal] (This is an ASCII version of what will show on the breadboard LEDs) ¹[Image: You can press R in the main menu to toggle this view] ²[Image: You can press R in the main menu to toggle this view] ³[Image: You can press R in the main menu to toggle this view] ```jython GPIO Pad └─ ⁱ1⁰ ⁱ2⁰ ⁱ3⁰ ⁱ4⁰ ₁5₀ ₁6₀ ₁7₀ ₁8₀ [Tap pads to choose which `GPIO` (left side for input, right side for output)] └─ [Tap a row to connect it to] (or if you were already "holding" a node, it'll connect there) ``` ¹[Image: You can press R in the main menu to toggle this view] The 4 `user pads` will be remappable in the future, but for now, `top_guy` is `routable UART Tx` and `bottom_guy` is `routable UART Rx`, and `building` pads are `Current sense` + and -. The **building pads** have multiple functions: - In `idle mode`: Override colors for net highlighting (see [Idle Mode Interactions](#idle-mode-net-highlighting)) - In `connect`/`remove` mode: Access **Current Sense (I+/I-)** with marching ants visualization! [Image: userPads] ### Current Sensing with Marching Ants When you tap either building pad in connect or remove mode, you'll get access to the current sense inputs (I+ and I-). When both I+ and I- are connected to different nets in your circuit: 1. A virtual wire appears between the two nets containing the `I Sense` nodes 2. Animated "marching ants" flow along this wire showing current direction The animation automatically picks the where to put the virtual "wire". It will search other nodes on the same nets that `I sense +` and `I sense -` are on it prefers places where they're on the same level so it can actually draw a connecting wire and not just be vertical lines. Your browser does not support the video tag. !!! warning `I Sense +` and `I Sense -` go on different nets but they're shorted internally They're two ends of a 2Ω shunt resistor, so remember that these will be shorted together. You measure current in series so this is expected, but it's super easy to forget. Take this warning as the equivalent of your multimeter yelling at you when you have the probes in the current holes and have it set to voltage. --- ## Undo / Redo Every change you make to your circuit gets recorded into an in-memory history ring, and there are two ways to walk back through it: a fast probe-button gesture for one-step undo/redo, and a full scrub menu on the click wheel for going further. ### Double-tap the probe buttons `Double-tap Remove -> Undo (reverts the last change)` `Double-tap Connect -> Redo (reapplies a previously undone change)` - The `logo` flashes **yellow** for ~600 ms - The OLED shows a 2-line toast like: ### History scrub via the click wheel If you want to walk back further than one or two steps and *see* each state on the breadboard as you go: 1. Open the menu (short `click` the wheel) 2. Find the **History** entry, `click` in 3. Now turning the wheel scrubs through every recorded transaction: - **CW** = step backward in time (one txn older) - **CCW** = step forward (newer / into redo land) 4. The breadboard updates **in real time** as you scrub - actual crosspoints flip and `LED`s change, so you can see exactly what each state looked like 5. The OLED shows the position (`-3/12`) and the txn label (`connect 50-46`) at each step 6. **Encoder `click`** OR **probe `Connect`** = commit (cursor stays where you stopped) 7. **Encoder `hold`** OR **probe `Remove`** = cancel (rewinds to where you were when you opened the menu) #### Caveats - **History is fresh on every boot** - undo doesn't survive a power cycle. This is intentional: nothing about your saved `slot` files is touched by undo, only the in-session ring of changes you've made since powering on. - The ring holds **~16,000 ops / 4,000 transactions on V5 hardware** (PSRAM), or ~512 ops on SRAM-only builds. Older edits roll off as you make new ones - waypoint decimation keeps the recent history dense and the older history sparse, so the ring covers a long timespan without bloating. ---- ## Idle Mode Net Highlighting The main thing is that there's a lot more interaction that can be done outside of any particular mode (like not probing and the logo is rainbowy, I'm gonna call this idle mode here until I think of a good name) [Image: idle] Here's what's new (all of this is in idle mode): ### Basic Interactions - **Tapping nets highlights them** as before, but there's a slightly different animation on the `row` you have selected from the whole `net` - **The click wheel scrolls through highlighting `rows`** as if you tapped each one ### Row Selection Actions With a `row` selected, here's what you can do: #### Connect Button - `connect` button will bring you into probing mode with the highlighted row already selected and then spit you back out to `idle` mode once you've made a connection to another row, or click `connect` again to exit #### Remove Button - `remove` will remove the highlighted `node` ### Measurement Display - if the highlighted row is a `measurement` (`gpio input` or `adc`) it will print the state to serial and the oled ### Output Toggle - if the highlighted row is an `output` (`gpio output`, I'll eventually do `dacs` too) clicking the `connect` button will toggle it `high` / `low`. The `remove` button will *just* unhighlight the net (there were some choices here, like make each button assigned to high / low or allow removing them, but this felt like the best way after trying them all). I will eventually add a setting for the toggle repeat rate (set to 500ms now) and a way to set it freewheeling as a clock. --- # The App ## Installation guide ### The Jumperless App is now on PyPi! The easiest way to get started is with pip: ```bash pip install jumperless ``` Then run it with: ```bash jumperless ``` **Note:** If the app version shows less than the latest release, `pip` defaults to a local version if it's available. In that case, run: ```bash pip install --no-cache-dir --upgrade jumperless ``` to make sure it grabs the latest version. The app repo is at [https://github.com/Architeuthis-Flux/Jumperless-App](https://github.com/Architeuthis-Flux/Jumperless-App) ### Alternative: Download Pre-built Binaries #### Find the latest release [https://github.com/Architeuthis-Flux/JumperlessV5/releases/latest](https://github.com/Architeuthis-Flux/JumperlessV5/releases/latest) The link above will magically lead you to the latest version, and will look something like `https://github.com/Architeuthis-Flux/JumperlessV5/releases/tag/5.2.0.0` **At the bottom under Assets, download the Jumperless App for your OS** ### Windows - `Jumperless.exe` - `Jumperless-Windows-x64.zip` ### macOS - `Jumperless_Installer.dmg` - `Jumperless_macOS.zip` ### Linux - x86 `Jumperless-linux-x86_64.tar.gz` (if you're not sure which flavor of Linux, use this one) - arm64 `Jumperless-linux-arm64.tar.gz` ### Python 1. download `JumperlessWokwiBridge.py` and `requirements.txt` 2. open your favorite terminal, navigate to the folder where you downloaded the two files above. 3. `pip install -r requirements.txt` # run this command to install the needed Python libraries 4. `python3 JumperlessWokwiBridge.py` # open the app, will update firmware if there's a newer version --- Now that I've lifted my self-imposed ban on VT100 commands (for compatibility and me-spending-too-much-time-on-them reasons, but, YOLO), we've got colors now! But that's like the *least* cool thing the new app can do, here's a list of what's new: ## What It Does - **Firmware updating** should be pretty reliable when there's a new version (falls back to instructions for how to do it manually) - **Command history and tab completion**, up arrows will go through past commands and are persistent after closing - **Properly detects** which port is the main Jumperless Serial and which is routable UART - **Arduino flashing from [Wokwi](https://wokwi.com/)** works once again and is a lot more solid - It installs [arduino-cli](https://github.com/arduino/arduino-cli) on first startup and uses it pull in libraries, compile, and flash an arduino Nano in the header - If the routable UART lines aren't connected when the app detects a change in the sketch file, it will connect them to flash the new code and then return them to how they were - [avrdude](https://github.com/avrdudes/avrdude) output is shown in real time (you'd be amazed how difficult this was) - **Direct Wokwi circuit import** - Copy diagram.json from Wokwi and import it with the `W` command (see below) - **No longer a janky pile of garbage** ## Local Arduino Sketch Support **You can set a `slot` to point to a local Arduino sketch.ino file and it will flash if it detects a change** - If you don't like using Arduino IDE or Wokwi and prefer using `vim` or `emacs` or whatever, now you can let the app handle the flashing stuff and just edit an .ino file. - In the app, type `menu` then `slots` and instead of entering a link to a Wokwi project, just give it a path to a file (this will be saved so you can unassign it and pick it later by name) - (This one is so fucking sick) ## Launch Scripts - Launch scripts included to easily run it from your favorite terminal emulator and not just the system default (terminal.app on macOS, Powershell on Windows, idk on Linux), just go to the directory in a terminal and run the script in [tabby](https://tabby.sh/) or whatever - The launcher *should* kill other instances (and close their windows) that happen to be open because it's such a common issue for me at least - Linux people are no longer red-headed stepchildren, there are proper tar.gz packages now for you nerds --- ## Importing Circuits from Wokwi You can design circuits in the [Wokwi online simulator](https://wokwi.com) and import them directly to your Jumperless with the `W` command, or use the Jumperless App and it'll pull it from your project automatically and live update. ### Direct Link Import You can now just dump a Wokwi link into the app at any time and it'll work: ``` Menu ~~~~~ x = clear all connections + = add connections - = remove connections https://wokwi.com/projects/424432011346848769 Enter a name for this new project: cool project zone ✓ Saved 'cool project zone' to project library ✓ 'cool project zone' assigned to active slot 0 URL: https://wokwi.com/projects/424432011346848769 The project will start updating automatically ``` ### How to manually Import from Wokwi 1. **Design your circuit** on [wokwi.com](https://wokwi.com) 2. **Click on the `diagram.json` tab** in the Wokwi editor 3. **Copy all the JSON content** (Ctrl+A, Ctrl+C or Cmd+A, Cmd+C) 4. **In Jumperless, type `W`** and press Enter 5. **Paste the JSON** (Ctrl+V or right-click → Paste) 6. The parser automatically detects when the JSON is complete and imports it! ### Supported Wokwi Components - **Half breadboard** - Wokwi's breadboard maps directly to Jumperless rows - **Arduino Nano** - All pins (D0-D13, A0-A7) (GND, 5V, 3.3V, and RST pins are hardwired and don't do anything) - **Logic Analyzer** - Channels map to GPIO: D0-7 → GPIO 1-8 - **Wire colors** - Wokwi wire colors preserved - **Rail voltages** - Detected from text labels in Wokwi - **VCC and GND Nodes** - VCC maps to the `TOP_RAIL` [Image: LogicAnalyzerMappingV5] **Note:** The app still works with the OG Jumperless and those original mappings remain the same. ### Wire Color Mapping **Wire colors will match the ones you set in Wokwi!** The new Wokwi parser sends the entire `diagram.json` from Wokwi and parses it on the Jumperless, which means color information gets preserved. [Image: wokwiColor-2] [Image: wokwiColor-1] All Wokwi wire colors are preserved and displayed on the breadboard LEDs: `red`, `orange`, `yellow`, `green`, `blue`, `violet`, `purple`, `magenta`, `cyan`, `white`, `gray`, `black`, `brown`, `limegreen`, `gold` **Note:** Black wires let the Jumperless auto-assign a color. If you leave all the wires green (the default in Wokwi) or make a wire black, it'll just auto assign colors. **About color assignment:** There is some weirdness because colors in Wokwi are applied to `bridges` (a pair of `nodes`) while color in the Jumperless gets assigned to `nets` (a collection of connected `nodes`). So if you have a bunch of things electrically connected together with different wire colors, it'll just pick one. It tries to pick unique colors first (no other nets with that same color), but if it can't, it'll shift the hue a bit so it's still that color but you can hopefully tell them apart. ### Rail Voltage Detection Add a text label in your Wokwi diagram to specify rail voltages: ``` top rail 5.5V bottom rail 3.5V ``` The Jumperless parser will automatically detect these values and set the rails accordingly ### Command Variants ``` W # Paste JSON, save to active slot W 5 # Paste JSON, save to slot 5 W /file.json # Load from file, save to active slot ``` ### After Import Use `<` to cycle through slots to activate your imported circuit, or it will be active immediately if imported to the current slot. ## Terminal Compatibility Or you can use any terminal emulator you like, [iTerm2](https://iterm2.com/), [xTerm](https://invisible-island.net/xterm/), [Tabby](https://github.com/Eugeny/tabby), [Arduino IDE](https://www.arduino.cc/en/software/)'s Serial Monitor, whatever. The TUI is all handled from the Jumperless itself so it just needs something to print text. --- # OLED Support First, get yourself one of these bad boys (literally any of these are fine.) [[Image: oled Medium]](https://www.amazon.com/dp/B0CDWQ2RWY/) https://www.amazon.com/MakerFocus-Display-SSD1306-3-3V-5V-Arduino/dp/B079BN2J8V [Image: batchone-11] Ignore the really cool LEDs. ## Installation They should friction fit into the SBC/SMD/OLED board included with your Jumperless V5. [Image: SBCBP-4 copy] Yo This should copy basically any text printed on the breadboard, some people have trouble reading text on the breadboard LEDs, which is why I added all this. ## Connection To connect the data lines to the Jumperless' GPIO 7 and 8, just use the menu option `.` (that's a period). It will try to find the OLED on the I2C bus, after a few failed attempts, it'll automatically disconnect to free up GPIO 7 and 8. ## Auto-Connect on Boot If you want to use this all the time, there's a config option to connect the OLED on startup. You can just paste this into the main menu: ``` `[top_oled] connect_on_boot = true; ``` ## Lock Connection Locking the connection to the OLED ensures that it stays connected even when you enter a complete `node` list. So if you're using Wokwi or manually adding connections in a file, you don't need to add `GPIO_7 - D2` and `GPIO_8 - D3` to keep the I2C connected to the OLED. ``` `[top_oled] lock_connection = true; ``` ## Custom Startup Message You can customize what appears on the OLED when your Jumperless boots up. There are two options: text messages or custom bitmap images. ### Text Message Set a custom text message to display on the OLED at startup (max 32 characters): ```jython `[top_oled] startup_message = Your Message Here; ``` This message will appear after the Jumperless logo on boot. ### Bitmap Image Display a custom bitmap image at startup by just giving it a path on the filesystem. ```jython `[top_oled] startup_message = /images/mylogo.bin; ``` **Requirements:** - Image must be a bitmap file (`.bin` format) with 4-byte header - Recommended size: 128×32 pixels (standard OLED size) - Use the built-in [Bitmap Editor](#bitmap-editor) to create or edit images - Store images in the `/images/` directory on the Jumperless filesystem ## Display Dimensions If you have a different sized OLED (like 128x64), you can set the dimensions: ```jython `[top_oled] width = 128; `[top_oled] height = 64; ``` ## Advanced GPIO Configuration You can change both the GPIO used for the display or the rows it connects to with the config options: ```jython `[top_oled] sda_pin = 26; `[top_oled] scl_pin = 27; `[top_oled] gpio_sda = GP_7; `[top_oled] gpio_scl = GP_8; `[top_oled] sda_row = D2; `[top_oled] scl_row = D3; ``` ## Connection type ```jython `[top_oled] connection_type = rp6_rp7; ``` ## Bitmap Editor The built-in bitmap editor lets you create and edit OLED images directly on your Jumperless using your terminal and the clickwheel. Your browser does not support the video tag. ## Accessing the Bitmap Editor ### From File Manager 1. Open the file manager from the main menu 2. Navigate to a `.bin` bitmap file 3. Select the file to open it in the editor ### Creating a New Image You can create a new bitmap file from the file manager: 1. Navigate to where you want to create the file (e.g., `/images/`) 2. Press `n` for "new file" 3. Name it with a `.bin` extension (e.g., `mylogo.bin`) 4. The editor will automatically create a blank 128×32 bitmap (or whatever your OLED dimensions are set to in config) ## Editor Interface The bitmap editor displays your image in the terminal and on the OLED (if connected). You'll see: - **Main canvas**: Your bitmap rendered using block characters - **Status bar**: Filename, dimensions, cursor position, and save status - **Menu bar**: Quick access to View, Encoder, Draw modes, Save, and Quit - **Help panel**: Keyboard shortcuts and hardware control reference ### View Modes Press `m` to cycle through three display modes: 1. **Full Block Mode** (1:1 pixel mapping) - Each character = 1 pixel 2. **Half Block Mode** (2:1 vertical compression) - Each character = 2 pixels vertically - Fits 128×32 images on smaller terminals 3. **Quarter Block Mode** (2×2 compression) - Each character = 2×2 pixels (4 pixels total) - Fits larger images on screen ## Navigation ### Moving the Cursor **Keyboard:** - Arrow keys or `W/A/S/D` keys - Vim keys: `j` (down), `k` (up), `l` (right) **Hardware:** - **Clickwheel encoder**: Rotate to move cursor - **Probe switch**: - `Select` position → Horizontal movement - `Measure` position → Vertical movement - Press `/` to toggle encoder direction (H/V) independently ## Editing Pixels ### Editing Methods The editor has three draw modes (cycle with `.` key) to pick what happens when you press `enter`/`space`/`encoder click`: 1. **Toggle Mode** (default): Flips pixel state (ON↔OFF) 2. **Set Mode**: Always turns pixels ON (draw) 3. **Clear Mode**: Always turns pixels OFF (erase) Or just use these keys to do it directly and not worry about the mode: - `z` = Set pixel (turn ON) - `x` = Clear pixel (turn OFF) - `c` = Toggle pixel ## Menu Bar Navigation When the cursor reaches the bottom edge and you press down, you enter the menu bar: **Navigation:** - Left/Right arrows: Move between menu items - Enter/Space: Activate selected item - Up: Exit menu bar **Menu Items:** - **View**: Cycle display modes (Full/Half/Quarter) - **Enc**: Toggle encoder direction (H/V) - **Draw**: Cycle draw modes (Toggle/Set/Clear) - **«Save»**: Save file and return to editing - **«Quit»**: Exit editor (prompts if modified) ## Saving and Quitting - **Ctrl+S**: Quick save - **Ctrl+Q**: Quit (prompts to save if modified) - **h or ?**: Show help screen The editor automatically adds the 4-byte header (width and height) when saving, making the file compatible as a startup image. ## Example Workflow: Creating a Startup Logo 1. Open file manager, navigate to `/images/` 2. Create new file: `mylogo.bin` 3. Editor opens with blank 128×32 canvas 4. Switch to Half Block mode (`m`) for better overview 5. Use clickwheel to navigate, Connect button to draw 6. Save with Ctrl+S 7. Set as startup image: - By editing the config file: ``` `[top_oled] startup_image = /images/mylogo.bin``` - Or use the click menus `OLED` > `Startup message` > `image` > (scroll through all the images and `click` to select) 8. Reboot or enter/exit the click menu to see your custom logo ## Editor Screenshots Full size view (1:1 pixel mapping): ```jython ███████ ██████ ███████ ██ ██ ██████ █████ ███ ██ ██ ██ █ █ ██████ ██████ ████ ██ ██ ██ ██ █ █ ███ ██ █████ ███ ████ ████ ██ ████ █ █ ██ █ █ █ ██ ██ █ ██ █ ████ █ ██ ███ ██ ███ ███ █ ██ ██ █ ██ █ ██ █ ██ ██ ██ █ ███ ██ █ █ ███ █ ██ ██ ██ ██ ██ █ ████ █ ██ █ ██ ██ █ █ █ █ █ █████ █ ██ █ ████ ████ █ ████ ██ ██ █ ██ █ █ █ █ ████ █ ██ █████ █ ██ █ █████ ██ ████ ██ ████████ ██ █ ██ █ █ ██ █ █████ █ ███████ █████ █ ██ █ ████ █ █████████ █████ ██ █ ██ █ ██ █ ██████ █ █████ █ █ ██ █ ███ █ ██ █ ████ ████ ███ ██ █ ███ █ ██ █ ██ ██ █ ███ ██ █ █ █ ██ █ ███████ ██ ███ █ ██ ██ █ ███ █ █ █ █ █ █ ██ ██ █ █ █ ██ ██ ██ ████ ███ ██ █ ██ █ ███ █ █ █ █ █ ██████ ██ █ █ █ ██ ██ █ ██ ██ ███ ██ ██ █ ████ █ █ █ █ █ ██ ████ █ ██ █ ██ ██ █ ███ █ ████ ██ █ █ ████ █ █ █ ██ █ █ █ ████ ███ █ ██ ██ ████ ██ ████ █ █ ██ ███ █ █ ████ █ █ ██ ███ ███ █ █ █████ ████ █ ███████ █ ██ █ ███ █ █ █ ███ █ ██ ██ ████ █ █ ████ ███ ███ ████ █ █ █ ███ ██ █ █ ██ ██ ████ ██ ███ █ █ █ ███████ ███████ ██ ██ █ █ █ ███ ██ █ ██ ██ ██ ███ █ ██ █ █ ████ ████ ████ ██ █ █ ████ █ █ █ ██ ██ ██ █ ████ ██ █ ██ ██ █ ████ ██ ██ ██ █ ██ ██ ██ █ ██ ██ █████ █ ██████ ██ ███████ █ ███ ███ ██ █ ██ █ █ █ ███ ██ ███ ████ █ █████ ██ ██ ███ ██ ██ █ ██ ███ ██ █ █ ██ ███ █ ██ █ ██ ██ ██ ███ █ █ ████ ████ ██ █ ████ █ █ █ █ █ █ ██ ██ ███ ██ ███ ███████ ██ ██████████ █ █ ██ ██ █ █ █ █ █ ██ ████ ████ ████ ██████████████ ███████████ █████████ █ ██ ██ ██ █ █ ██ ███ ██ ███ ███████████ ██████████████████ ████████ █████████ ███████ ██ ██ ███ ██ ██ ██ ████████████ ███████████ █████ █████ █████████ ██████ ██████ ██ ██ ████████ ██████ █████ █████ █████████ ███ ███ ██████ ███ ███ ██████ ████ ███ ███ █████ █████████ ████ ██ ███████ /images/bubbleJump.bin | 128x32 | (64,16) | Saved View:Full | Enc:V | Draw:CLR | «Save» | «Quit» ⟨Clickwheel > ↺ / ↻: move H/V | Click: toggle pixel ⟩ ⟨ Probe Buttons > Connect:set | Remove:clear | Switch > Select:H | Measure:V ⟩ ⟨Terminal > [z]:set [x]:clear [c]:toggle pixel | [m]:Cycle View | [/]: Enc H/V | ctrl+S:Save | ctrl+Q:Quit | [?]:Help ⟩ ``` Half Block view (2:1 vertical compression - each character is 2 pixels tall): ```jython ▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▀▀▀▀▀▀█▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▀▀▀▀▀█▄ ▄█▀▀▀█▄ ▄█▀▀ ▀█ ▄▀▀ ▀█▄▄▀ █▄ ▄█▀▀▀█ ▄▄▄▄ ▄▀▀█▄ ▄▄▄ ▄█▀▀█▄▄▀▀▀▀ ▀█▄▄▀▀▀▀ ▀▄▀ ▀█▄▀ █▄ ▄▀ ██ █▀ ▄▄ █ ▄█▀ █▀ ▀█▄▀ ██▀ ▀█ █ █ ▀█▀ █ ▄▄▄▄▄ ▀█ ██ █▀ ▄▄██ ▄██▄ █ ████ ▄█ ██ █ ██ █ █ ▄█ █ ▄████ █ ▄▄▄▄▄██ █████ █ ██ █ ▄███▀▀ ▀█ ████▄▄▄██ ▀█████▀▀ ██ █ ██▄ █ ██ █ ██▀▀██ █ ███▀▀ █▄ █ ▀█ █ ██▀ █ ██▄▄▄▄▄ █▄ ▀███ ▀▀▀█ ▀▀█▄ ██ █ ███ █ ▀ █ █ █ █ ██▄▄▄▄ ██ █ █ █ ██ ██ ▀▀ ▀█▀█▄ ▀▀█▄ ▀█▄▄ █▄ ▀█ █ ████ █ █ ▀▄ ▄█ █ ▀▀ ▀█▀█ █▄▄█▀ ▄█▄ █▀ ██ ▄█ ▀▀█▄▄▄ █▄ ▀███▄ ▀█ █▄ ▀█ ███ █ ▄ █ ███▀ █ ▄█ ▄█ ▀▀▀ ▄███ █ █ ▄███▀▀ ▀███ █▄█▀▀████ █ █ █ ███ ██ █ ▄█ ██ ██ ▄██▀▀ ▀█ ██▀ █ █ ▄▄▄█ ████▀▀█▄▄█▀▀▀██▀ █▀ ▀▀ █ ▄█▀▀█▄ █ █▄ ▀ ██ ██▄██ █ ▄▄▄▄▄█▀██ ██▄▄▄▄▄ █ ██▄ ▀█▄█ ██▀▀ ▀█ ▀▀ ██ █ █▀ █ ▀█▀ ██ ███ █▀██ █ ▀██▀▀ ██ ▀▀ ▀██ ██▄ ▀█ █ ▄██▄ ███▄ ▄█▀ █ █▀██ ▄█ █ █ █ █ █ ▀█▄ ▄██▄ ███▄ ▄██▄ ▄▄███▄▄▄▄▄▄███▀███▄▄▄▄▄▄▄█▀ ▀█████████ █▄ ██ ██▄ ▄█▀ █▄ ▄█ ██▄▄███▄▄▄██ ▀██▄▄▄▄▄████▀▀█████ ▀█████▀▀█████████▀ ▀██████▀ ▀▀██████▀ ▀▀▀▀▀▀▀ ██▄ ▄██ ▀██████▀ ▀████▀ ▀███▀ ▀███▀ ▀█████▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀ ▀███████▀ ▀▀▀▀ ▀▀ /images/bubbleJump.bin | 128x32 | (64,16) | Saved View:Half | Enc:V | Draw:CLR | «Save» | «Quit» ⟨Clickwheel > ↺ / ↻: move H/V | Click: toggle pixel ⟩ ⟨ Probe Buttons > Connect:set | Remove:clear | Switch > Select:H | Measure:V ⟩ ⟨Terminal > [z]:set [x]:clear [c]:toggle pixel | [m]:Cycle View | [/]: Enc H/V | ctrl+S:Save | ctrl+Q:Quit | [?]:Help ⟩ ``` Quarter Block view (2×2 compression - each character is 4 pixels): ```jython ▄▄▄ ▄▄▄▖ ▗▞▀▀▜▖ ▄▄▄ ▗▄▄▖▗▄▞▀▀▙ ▟▀▜▖ ▗▛▘ ▝▌▗▀ ▜▄▘ ▙ ▟▀▜▗▄▖ ▞▜▖▗▄ ▗▛▜▄▀▀ ▝▙▞▀▘ ▚▘ ▝▙▘ ▙ ▗▘ ▐▌ ▛ ▄ ▐ ▗▛ ▛ ▜▞ █▘▝▌▐ ▌ ▜▘ ▐ ▗▄▄ ▜ █ ▛ ▄█ ▟▙ ▌ ▐█▌ ▟ ▐▌ ▌ ▐▌ ▐ ▌▟ ▌ ▟█▌ ▐ ▗▄▄█ ██▌ ▐ █ ▌ ▗█▛▘▜ ██▄▟▌ ▝██▛▘ █ ▌ ▐▙ ▐ ▐▌ ▌ █▀█ ▐ █▛▘▐▖ ▌ ▜ ▐ ▐▛ ▌ ▐▙▄▄▐▖ ▝█▌▀▜ ▀▙ ▐▌ ▌ ▐█ ▐ ▘ ▌ ▌ ▐ ▐ █▄▄▐▌ ▐ ▐ ▐ ▐▌ █ ▝▘ ▝▛▙ ▀▙▝▙▖ ▐▖ ▝▌ ▌ ▐█▌ ▐ ▌ ▚ ▟ ▐ ▀ ▝▛▌ ▐▄▛ ▟▖ ▐▘ █ ▗▌▝▜▄▖ ▐▖▝█▙ ▜ ▙ ▜ █▌ ▐ ▖ ▌ ▐█▘ ▐ ▗▌▟ ▝▀ ▗█▌ ▐ ▐ ▟█▀ ▜█ ▙▛▜█▌ ▐ ▐ ▐ █▌ ▐▌ ▌▗▌ █ █ ▗█▀ ▜ █▘▌ ▐ ▄▟ ██▀▙▟▀▜▛ ▛ ▀ ▐ ▟▀▙▐ ▐▖ ▝ ▐▌ █▟▌ ▐ ▄▄▟▜▌ ▐▙▄▄▐ ▐▙ ▝▙▌ ▐▛▘▝▌ ▝▘ ▐▌ ▌ ▛ ▌ ▝▛ ▐▌ █▌ ▐▜▌ ▐ ▜▛▘▐▌ ▀ ▝█ █▖ ▝▌ ▌ ▟▙ ▐█▖ ▟▘ ▌ ▛█ ▗▌▌ ▐ ▌ ▐ ▐ ▝▙ ▗█▖ █▙ ▗█▖ ▗▟█▄▄▄█▛█▙▄▄▄▛▝████▌ ▙ ▐▌▐▙ ▟▘▙ ▟ █▄█▙▄█ ▜▙▄▄██▀██▌▜██▀████▛▝███▘ ▝▜██▛ ▝▀▀▀ ▐▙ ▗█ ▜██▛ ▜█▛ ▜█▘▜█▘ ▝██▛▀ ▝▀ ▀▘ ▝▀▀▘ ▜███▘ ▀▀ ▀ /images/bubbleJump.bin | 128x32 | (64,16) | Saved View:Qtr | Enc:V | Draw:CLR | «Save» | «Quit» ⟨Clickwheel > ↺ / ↻: move H/V | Click: toggle pixel ⟩ ⟨ Probe Buttons > Connect:set | Remove:clear | Switch > Select:H | Measure:V ⟩ ⟨Terminal > [z]:set [x]:clear [c]:toggle pixel | [m]:Cycle View | [/]: Enc H/V | ctrl+S:Save | ctrl+Q:Quit | [?]:Help ⟩ ``` The output of `?` ```jython === Bitmap Editor Help === Navigation: Encoder wheel - Move cursor (H or V mode) Arrow keys / WASD - Move cursor j/k/l (vim) - Move cursor Down at bottom edge - Enter menu bar Editing: Encoder click - Apply current draw mode at cursor Enter / Space - Apply current draw mode at cursor Connect button HOLD - Set pixels while held (draw lines) Remove button HOLD - Clear pixels while held (erase lines) Direct Pixel Actions (keyboard): z - Set pixel at cursor (draw) x - Clear pixel at cursor (erase) c - Toggle pixel at cursor Draw Mode Control: . - Cycle draw modes (Toggle/Set/Clear) Hardware Controls: Probe switch SELECT - Encoder horizontal movement Probe switch MEASURE- Encoder vertical movement Display: m - Cycle view mode (Full/Half/Quarter) / - Toggle encoder H/V movement Menu Bar (Down at bottom edge): Left/Right arrows - Navigate menu items Enter / Space - Activate menu item (cycle/Save/Quit) Up / Escape - Exit menu bar Menu Bar Items: View - Cycle display mode (Full/Half/Quarter) Enc - Toggle encoder direction (H/V) Draw - Cycle draw mode (Toggle/Set/Clear) «Save» - [Button] Save file and exit menu «Quit» - [Button] Quit editor (prompts if modified) File: Ctrl+S - Save file Ctrl+Q / ESC - Quit (prompts if modified) h / ? - Show this help Cursor Colors: Green background - Pixel is OFF Red background - Pixel is ON ``` ## Bitmap File Format The editor works with `.bin` files in two formats: **With Header (Recommended):** - 4 bytes: Width (16-bit little-endian) - 2 bytes: Height (16-bit little-endian) - Remaining: Bitmap data (MSB-first, row-major) - Example: 128×32 = 4 header + 512 data = 516 bytes total **Raw Format:** - Just bitmap data, dimensions inferred from file size - 512 bytes → 128×32, 1024 bytes → 128×64, etc. The editor automatically adds headers when saving, making files ready to use as startup images. ## Converting External Images Want to use your own images? The JumperlOS repository includes Python scripts to convert PNG/JPG images to OLED bitmaps: **Location:** `JumperlOS/scripts/image_to_oled_bitmap.py` **Usage:** ```bash python image_to_oled_bitmap.py input.png output.bin --width 128 --height 32 ``` The script will: 1. Resize your image to fit the OLED dimensions 2. Convert to 1-bit (black/white) format 3. Save with proper header format 4. Output is ready to use as a startup image or edit in the bitmap editor Then you can mount your Jumperless's filesystem as a mass storage device with `U` and drop it into the `/images/` folder. ### Check out the [`/scripts` folder](https://github.com/Architeuthis-Flux/JumperlOS/tree/main/scripts) in the [JumperlOS repo](https://github.com/Architeuthis-Flux/JumperlOS/tree/main), there are a few other scripts related to dealing with bitmaps. --- ## Using the OLED from MicroPython The OLED display has a comprehensive MicroPython API for programmatic control. You can display text with multiple fonts and sizes, show bitmaps, manipulate pixels directly, and even redirect Python's `print()` output to the OLED. ### Quick Start ```jython import jumperless as j import time # Basic text display j.oled_connect() j.oled_print("Hello!", 2) time.sleep(2) j.oled_clear() ``` ### Text Sizes and Scrolling The OLED supports three text size modes: - **Size 0**: Small scrolling text - perfect for terminal-like output with multiple lines - **Size 1**: Normal centered text - **Size 2**: Large centered text (default) ```jython import jumperless as j import time # Set default text size j.oled_set_text_size(0) # Small scrolling text # Display multiple lines for i in range(10): j.oled_print(f"Line {i+1}") time.sleep(0.3) # Switch to large text j.oled_set_text_size(2) j.oled_print("BIG TEXT") ``` ### Print Redirection for Debugging One of the most useful features is print redirection - all `print()` statements can automatically appear on both the serial console **and** the OLED: ```jython import jumperless as j # Enable print copying j.oled_copy_print(True) # These appear on both serial AND OLED print("Starting test...") voltage = j.adc_get(0) print(f"Voltage: {voltage:.2f}V") print("Test complete!") # Disable when done j.oled_copy_print(False) ``` This is perfect for debugging projects where you don't have easy access to the serial console. ### Multiple Fonts Choose from 11 different font families: ```jython import jumperless as j # List all available fonts fonts = j.oled_get_fonts() print(fonts) # Set a fun font j.oled_set_font("Jokerman") j.oled_print("Fun!", 2) # Switch to monospace for code j.oled_set_font("Courier New") j.oled_print("Monospace", 2) ``` ### Display Bitmaps Show bitmap images stored on the filesystem: ```jython import jumperless as j # One-liner to display a bitmap j.oled_show_bitmap_file("/images/jogo32h.bin", 0, 0) # Or load and display separately j.oled_load_bitmap("/images/logo.bin") j.oled_display_bitmap(0, 0, 0, 0) ``` ### Graphics and Pixel Control For custom graphics, you can manipulate individual pixels: ```jython import jumperless as j # Draw a box j.oled_clear() for x in range(20, 108): j.oled_set_pixel(x, 10, 1) # Top j.oled_set_pixel(x, 22, 1) # Bottom for y in range(10, 23): j.oled_set_pixel(20, y, 1) # Left j.oled_set_pixel(107, y, 1) # Right j.oled_show() ``` ### Advanced: Direct Framebuffer Access For maximum control, you can read and write the entire framebuffer: ```jython import jumperless as j # Get display dimensions width, height, size = j.oled_get_framebuffer_size() print(f"Display: {width}x{height}, {size} bytes") # Capture the screen fb = j.oled_get_framebuffer() # Save to file with open("/screen_capture.bin", "wb") as f: f.write(fb) # Restore later with open("/screen_capture.bin", "rb") as f: fb_data = f.read() j.oled_set_framebuffer(fb_data) ``` ### Complete API Reference For the full API documentation with all functions, parameters, and examples, see: **[MicroPython API Reference - OLED Display Section](09.5-micropythonAPIreference.md#oled-display)** The API includes: - Text size control (`oled_set_text_size`, `oled_get_text_size`) - Print redirection (`oled_copy_print`) - Font system (`oled_get_fonts`, `oled_set_font`, `oled_get_current_font`) - Bitmap functions (`oled_load_bitmap`, `oled_display_bitmap`, `oled_show_bitmap_file`) - Framebuffer access (`oled_get_framebuffer`, `oled_set_framebuffer`, `oled_get_framebuffer_size`) - Pixel manipulation (`oled_set_pixel`, `oled_get_pixel`) ### Example Projects **Animated Sine Wave:** ```jython import jumperless as j import math import time width, height, _ = j.oled_get_framebuffer_size() for offset in range(100): j.oled_clear(False) # Don't show() after clear to avoid flashing for x in range(width): y = int(height//2 + 10 * math.sin((x + offset) / 10)) if 0 <= y < height: j.oled_set_pixel(x, y, 1) j.oled_show() time.sleep(0.05) ``` **Sensor Monitor:** ```jython import jumperless as j import time # Monitor voltage with print redirection j.oled_copy_print(True) j.oled_clear() while True: voltage = j.adc_get(0) current = j.ina_get_current(0) print(f"V: {voltage:.2f}V") print(f"I: {current*1000:.1f}mA") time.sleep(1) ``` --- # Arduino Stuff ## UART Passthrough With an Arduino Nano in the header and the UART lines connected, anything on those lines should be passed through to the second serial port that shows up when you plug in your Jumperless. (You can also set the config option `[serial_1] print_passthrough = true;` and have it print on both. Don't worry about the baud rate, the Jumperless senses what the host computer is set to and changes the speed accordingly. ## Quick Connection Shortcuts The shortcuts to connect `D0` and `D1` to the Jumperless's UART `Tx` and `Rx` is `A` to connect, and `a` to disconnect. ## Automatic Flashing It will even sense when Arduino IDE is trying to upload code and twiddle the reset lines to allow you to flash code with just a single USB cable going to your Jumperless. **Tip:** You can also use [Wokwi](https://wokwi.com) with the Jumperless Bridge app for flashing - no need to even have the Arduino IDE open --- ## Commands from Routable UART You can send commands to the Jumperless from your Arduino (or anything connected to the routable UART) by wrapping them in XML-style tags. The tags are stripped out and the command is executed - the Arduino never sees them come back. ### Two Types of Tags There are two flavors of command tags, depending on what you want to do: #### `` Tags - Raw Commands These run exactly like you typed them in the main Jumperless menu. Use these for things like making connections with `f`, loading files, or any single-character menu command. #### `` Tags - Python Commands These run MicroPython commands directly. Perfect for `connect()`, `disconnect()`, `adc_get()`, `dac_set()`, and all the other Python hardware functions. The `` tag automatically prepends the `>` that normally tells the Jumperless "this is a Python command." ### Supported Tag Names Any of these work (use matching opening and closing tags): | Tag | Example | |-----|---------| | `` | `f 1-30` | | `` | `x` | | `` | `n` | | `` | `adc_get(0)` | --- ## Python Commands with `` Tags The `` tag is the most powerful way to control your Jumperless from Arduino code. It gives you direct access to all the MicroPython hardware functions. ### Basic Example ```cpp #define OPENJCOMMAND Serial.print(""); #define CLOSEJCOMMAND Serial.println(""); void setup() { Serial.begin(115200); delay(1500); // Give Jumperless time to boot } void loop() { // Read voltage on ADC channel 0 OPENJCOMMAND Serial.print("adc_get(0)"); CLOSEJCOMMAND delay(100); // Read the response while(Serial.available() > 0) { char c = Serial.read(); // Process the voltage reading... } } ``` ### Full Example - ADC Scanning Your browser does not support the video tag. This sketch connects ADC0 to different breadboard rows and reads the voltage at each one: ```cpp #define OPENJCOMMAND Serial.print(""); #define CLOSEJCOMMAND Serial.println(""); void setup() { pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); delay(1500); } int lastNode = 8; int node = 8; unsigned long delayTime = 60; void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(delayTime); node++; if (node > 60) { node = 1; } // Disconnect from previous node OPENJCOMMAND Serial.print("disconnect( ADC0," + String(lastNode) + ")"); CLOSEJCOMMAND delay(delayTime); // Connect to new node OPENJCOMMAND Serial.print("connect(ADC0 ," + String(node) + ")"); CLOSEJCOMMAND delay(delayTime); // Read the voltage OPENJCOMMAND Serial.print("adc_get(0)"); CLOSEJCOMMAND delay(delayTime); // Read response from Jumperless char response[30] = {0}; int idx = 0; while(Serial.available() > 0 && idx ` tags: ```cpp // Connections "connect(1, 30)" // Connect breadboard rows "connect(D13, TOP_RAIL)" // Connect Arduino pin to power "disconnect(ADC0, 15)" // Remove a connection "nodes_clear()" // Clear ALL connections // Analog I/O "adc_get(0)" // Read voltage (channels 0-4) "dac_set(0, 3.3)" // Set DAC output voltage "dac_set(TOP_RAIL, 5.0)" // Set rail voltage // Digital I/O "gpio_set(1, True)" // Set GPIO high "gpio_set(1, False)" // Set GPIO low "gpio_get(2)" // Read GPIO state // Current sensing "ina_get_current(0)" // Read current in amps "ina_get_voltage(0)" // Read shunt voltage ``` See the [MicroPython API Reference](09.5-micropythonAPIreference.md) for the complete list. --- ## Raw Commands with `` Tags Use `` tags when you want to send menu commands - the same ones you'd type in the serial terminal. ### Example - Making Connections ```cpp #define OPENJCOMMAND Serial.print(""); #define CLOSEJCOMMAND Serial.println(""); void setup() { pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); delay(1500); } int node1 = 1; int node2 = 8; unsigned long delayTime = 60; void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(delayTime); node1++; node2++; if (node1 > 60) node1 = 1; if (node2 > 60) node2 = 1; // Use the 'f' command to make a connection // Format: f - OPENJCOMMAND Serial.print("f " + String(node1) + "-" + String(node2) + "\n"); CLOSEJCOMMAND delay(delayTime); // Read any response char response[30] = {0}; int idx = 0; while(Serial.available() > 0 && idx `** for anything that's a Python function: `connect()`, `adc_get()`, `dac_set()`, etc. - **Use ``** for menu commands: `f`, `x`, `n`, `s`, etc. --- ## Wokwi Integration If you're using the [Jumperless Wokwi Bridge](https://github.com/Architeuthis-Flux/Jumperless-Wokwi-Bridge), you can flash your Arduino directly from Wokwi simulations - the bridge handles all the communication for you. --- # Config File To change any persistent settings that apply to the Jumperless as a whole, there's a `config` file. You can read it with `~` and edit settings by copying any of those lines, pasting it back, and changing the value to whatever you want it to be. ## Viewing Config.txt You can enter `~` to print the config. ```jython ~ copy / edit / paste any of these lines into the main menu to change a setting Jumperless Config: `[config] firmware_version = 5.6.5.15; `[hardware] generation = 5; `[hardware] revision = 7; `[hardware] probe_revision = 5; `[hardware] psram_installed = 0; `[dacs] set_dacs_on_boot = false; `[dacs] set_rails_on_boot = true; `[dacs] probe_power_dac = 0; `[dacs] auto_connect_probe = 1; `[dacs] limit_max = 8.00; `[dacs] limit_min = -8.00; `[debug] file_parsing = false; `[debug] net_manager = false; `[debug] nets_to_chips = false; `[debug] nets_to_chips_alt = false; `[debug] leds = false; `[debug] probing = false; `[debug] oled = false; `[debug] logo_pads = false; `[debug] logic_analyzer = true; `[debug] arduino = 0; `[debug] usb_mass_storage = false; `[routing] stack_paths = 2; `[routing] stack_rails = 3; `[routing] stack_dacs = 0; `[routing] rail_priority = 1; `[calibration] top_rail_zero = 1655; `[calibration] top_rail_spread = 18.80; `[calibration] bottom_rail_zero = 1655; `[calibration] bottom_rail_spread = 19.27; `[calibration] dac_0_zero = 1655; `[calibration] dac_0_spread = 19.53; `[calibration] dac_1_zero = 1650; `[calibration] dac_1_spread = 19.44; `[calibration] adc_0_zero = 8.95; `[calibration] adc_0_spread = 17.89; `[calibration] adc_1_zero = 8.91; `[calibration] adc_1_spread = 17.81; `[calibration] adc_2_zero = 8.97; `[calibration] adc_2_spread = 17.90; `[calibration] adc_3_zero = 8.89; `[calibration] adc_3_spread = 17.79; `[calibration] adc_4_zero = 0.00; `[calibration] adc_4_spread = 4.86; `[calibration] adc_7_zero = 9.24; `[calibration] adc_7_spread = 18.34; `[calibration] probe_max = 4055; `[calibration] probe_min = 25; `[calibration] probe_switch_threshold_high = 1.30; `[calibration] probe_switch_threshold_low = 1.14; `[calibration] probe_switch_threshold = 0.40; `[calibration] measure_mode_output_voltage = 3.27; `[calibration] probe_current_zero = 2.41; `[calibration] minimum_probe_reading = 85; `[logo_pads] top_guy = uart_tx; `[logo_pads] bottom_guy = uart_rx; `[logo_pads] building_pad_top = isense_pos; `[logo_pads] building_pad_bottom = isense-; `[logo_pads] repeat_ms = 100; `[display] lines_wires = wires; `[display] menu_brightness = -10; `[display] led_brightness = 10; `[display] rail_brightness = 55; `[display] special_net_brightness = 20; `[display] net_color_mode = rainbow; `[display] dump_leds = ; `[display] dump_format = image; `[display] terminal_line_buffering = 0; `[serial_1] function = passthrough; `[serial_1] baud_rate = 115200; `[serial_1] print_passthrough = false; `[serial_1] connect_on_boot = false; `[serial_1] lock_connection = false; `[serial_1] autoconnect_flashing = true; `[serial_1] async_passthrough = true; `[serial_1] tag_parsing = enabled; `[serial_1] flash_reset_type = avr; `[serial_2] function = micropython; `[serial_2] baud_rate = 115200; `[serial_2] print_passthrough = false; `[serial_2] connect_on_boot = false; `[serial_2] lock_connection = false; `[serial_2] autoconnect_flashing = false; `[top_oled] enabled = true; `[top_oled] i2c_address = 0x3C; `[top_oled] width = 128; `[top_oled] height = 32; `[top_oled] connection_type = i2c0; `[top_oled] sda_pin = 4; `[top_oled] scl_pin = 5; `[top_oled] gpio_sda = GP_4; `[top_oled] gpio_scl = GP_5; `[top_oled] sda_row = -1; `[top_oled] scl_row = -1; `[top_oled] connect_on_boot = true; `[top_oled] lock_connection = false; `[top_oled] show_in_terminal = false; `[top_oled] font = BerkMono; `[top_oled] startup_message = images/bubbleJumpThin.bin `[usb_cdc] ignore_dtr = false; ``` This is just a file on your filesystem called `config.txt` and just editing that file directly works too. ## Config Help There's also a `help` you can get to by entering `~?` ```c++ ~? Help for command: ~ Read config ~ = show current config ~names = show names for settings ~numbers = show numbers for settings ~[section] = show specific section (e.g. ~[routing]) Write config `[section] setting = value; = enter config settings (pro tip: copy/paste setting from ~ output and just change the value) Reset config `reset = reset to defaults (keeps calibration and hardware version) `reset_hardware = reset hardware settings (keeps calibration) `reset_calibration = reset calibration settings (keeps hardware version) `reset_all = reset to defaults and clear all settings `force_first_start = clears everything to factory settings and runs first startup calibration Help ~? = show this help ``` ## State File ## States vs Config States are now saved as YAML and we did away with the old text file format. `globalState` holds all connections, paths, and other circuit configuration in a single object that most of the code uses now. **State vs Config - What's the Difference?** - **State** stores things relevant to the currently loaded slot - connections, wire colors, rail voltages, GPIO settings. These change when you switch slots. - **Config** (config.txt) contains hardware-wide settings that apply to the entire Jumperless regardless of which slot is active. Rail voltages, GPIO settings, and other circuit-specific parameters now go with the state (in the YAML file) rather than config, so each slot can have its own power supply and GPIO configuration. ## State File Structure For things specific to the current `state` of the Jumperless, there's a YAML file that contains all the connections, colors (optional), `rail` / `DAC` voltages, `GPIO` directions and pulls, stuff like that. The idea is this defines a complete setup of a particular circuit that can be switched between in different `slots`. The Jumperless always boots at `Slot 0`, and you can switch to other `slots` with ` `Load` > `0-7` (it will show a preview of each one.) To save a copy of the currently `active slot`; `Slots` > `Save` > `0-7` will save a copy of the `active slot` to another `slot` and also make that target slot the `active`. ```jython ╭────────────────────────────────────╮ │ Current YAML State (RAM) │ ╰────────────────────────────────────╯ Active Slot: 0 Dirty Flag: NO (saved) ─── YAML Output ─── version: 2 sourceOfTruth: bridges bridges: - {n1: 38, n2: 44, dup: 2} - {n1: 21, n2: 28, dup: 2} - {n1: 48, n2: 55, dup: 2} - {n1: 4, n2: 2, dup: 2} - {n1: BUFFER_IN, n2: DAC0, dup: 1} nets: - {num: 4, nodes: [DAC_0, BUF_IN], name: "DAC 0", anim: true} - {num: 6, nodes: [38, 44], color: pink} - {num: 7, nodes: [21, 28], color: blue} - {num: 8, nodes: [48, 55], color: green} - {num: 9, nodes: [4, 2], color: amber} power: topRail: 3.30 bottomRail: 2.50 dac0: 3.30 dac1: 0.00 config: routing: {stackPaths: 2, stackRails: 3, stackDacs: 0, railPriority: 1} gpio: direction: [1,1,1,1,1,1,1,1,1,1] pulls: [0,0,0,0,0,0,0,0,0,0] pwmFrequency: [1.00,1.00,1.00,1.00,1.00,1.00,1.00,1.00,1.00,1.00] pwmDutyCycle: [0.50,0.50,0.50,0.50,0.50,0.50,0.50,0.50,0.50,0.50] pwmEnabled: [0,0,0,0,0,0,0,0,0,0] uart: {txFunction: 0, rxFunction: 1} oled: {connected: false, lockConnection: false} ─── Memory Usage ─── Connections: 5 State RAM: ~58048 bytes ``` ## Source of Truth Because the information in here is *sort of* redundant (the connections could be computed from just the `bridges` or the `nets` section on their own), there's a field called `sourceOfTruth` which is the section that actually gets parsed and then the other section is written with the computed values. (I haven't done much testing on changing this to `nets` so I'd probably just leave it on `bridges` for now.) There's some weirdness with how colors are applied, since the `source of truth` is the bridges, and the things that actually get colored are the `nets`, it'll take the colors from the `bridges` (if specified) and try to apply them to the `nets`. But since nets always have a single color (to show that they're connected), if you have `bridges` with different colors in the same `net`, it'll just pick one (don't ask me exactly how the logic chooses, idk.) If you specify a color to a `net` even with `sourceOfTruth: bridges` it should respect the `net` assignment over the `bridge` assignment. ## Switching Between Saved Circuits (Slots) The Jumperless has **8 slots** (0-7) where you can save different circuit configurations. Think of them like presets or save files. **Quick slot cycling:** - Type `<` in the terminal to cycle to the next slot **Other slot commands:** - `l 5` - Load slot 5 specifically - `Q` - Query which slot is currently active - `s` - Show a list of all saved slots When you make connections with the probe, they're automatically saved to whichever slot is currently active. See the [Glossary](99-glossary.md) for more details about slots. ## Editing State Files You can edit the YAML slot files and the board will pick up your changes! If you're editing the active slot in the onboard `eKilo` editor, it reloads when you quit the editor; if the Jumperless is mounted as a USB MSC device on your computer, changes are applied when you eject/unmount the drive. --- # Debug Views Look *Inside* your Jumperless ## Crossbar Array There's a new way to see what the 12 analog crossbar switches are up to, just enter `C` in the menu [Image: Crossbar View] Lowercase `c` will show a compact version [Image: Crossbar Compact] You can also set it to live updating mode with `c!` Your browser does not support the video tag. --- ## Bridge Array Enter `b` in the menu. This is generally the most helpful one for *me* to troubleshoot what's going on if your issue has anything to do with routing or connections. It probably looks like nonsense to you but I've been in it so long it makes perfect sense to me. [Image: Screenshot 2025-05-30 at 7 04 54 AM] --- ## Net List Enter `n` in the menu to show this one. If you have anything that's doing any measurement (`gpio` input or `ADC`s), it'll stay up and live update if any of them change. (And just like basically any menu not asking for input, entering anything will bring you back to the main menu.) [Image: Screenshot 2025-05-30 at 7 10 04 AM] --- # Automation & LLM Tools The Jumperless V5 is built to be driven by software — by your own scripts, by desktop apps, and by LLM agents (Cursor, Claude Desktop/Code, etc.). This page is the hub for **every** way to control the board from outside the menu, and a decision guide for picking the right one. The full hardware/API surface is documented in the [MicroPython API Reference](09.5-micropythonAPIreference.md) — this page is about *transport and tooling*, not the individual calls. --- ` prefix on port 1 | | You want to run **full scripts on the device** | MicroPython **Raw REPL** on port 5 | All of these ultimately drive the same MicroPython API on the device. The backchannel (port 7) is the exception: it is a read-only telemetry channel that never enters Python and never mutates state. ```mermaid flowchart LR subgraph host [Host / Agent] MCP[jumperless-mcp] SKILL[Agent Skill + jumperless.py] SCRIPT[your scripts] end subgraph dev [Jumperless V5] P1[port 1: menu + > Python] P5[port 5: Raw REPL] P7[port 7: USBSer3 backchannel] API[(MicroPython API)] end MCP - -> P5 SKILL - -> P5 SCRIPT - -> P1 & P5 P1 - -> API P5 - -> API P7 -. read-only .-> API ``` --- --> ## USB port structure The Jumperless enumerates as four USB CDC serial interfaces. On macOS they are named `JLV5portN`; on Linux they are sequential `/dev/ttyACMx`; on Windows they are sequential `COMx`. | Port | macOS suffix | Role | |------|--------------|------| | 1 | `JLV5port1` | Main terminal, menu, `>` one-liner Python, single-char commands | | 2 | `JLV5port3` | Arduino UART passthrough | | 3 | `JLV5port5` | MicroPython Raw REPL — **primary transport for scripts and the Agent Skill** | | 4 | `JLV5port7` | USBSer3 machine backchannel — `:help` (YAML), `:cmds`, read-only status verbs | > [!TIP] > The host helper `scripts/jumperless.py detect` (in the Agent Skill repo) > auto-finds the Raw REPL port and caches it, so you rarely need to name ports > by hand. > [!NOTE] > I will refer to these ports by their index (1st, 2nd, 3rd, 4th), macOS and Linux claim 2 ports per endpoint (one for each direction) so the numbering is 1,3,5,7 --- ## Communication methods ### 1. `>` Python one-liners (1st / main port) Prefix a single line of Python with `>` on the main terminal. Best for quick checks and single commands. ```jython > connect(1, 5) > v = adc_get(0) > print(f"V = {v:.2f}") ``` ### 2. MicroPython Raw REPL (3rd port) The canonical transport for anything beyond a one-liner: full scripts, loops, guided flows. This is what the Agent Skill's `jumperless.py` and the MCP server both speak under the hood. ```python import time for i in range(10): print(i, adc_get(0)) time.sleep(0.5) ``` ### 3. Arduino tags (2nd port, via UART) From an Arduino connected to the Jumperless headers: ```cpp Serial.print("connect(1, 5)"); // run Python Serial.print("n"); // menu command ``` ### 4. Single-character commands (1st port) Raw characters sent to port 1 trigger immediate menu actions — fast state dumps, board clears, etc. (`J` = JSON state, `x` = clear, `n` = list nets, …). ### 5. USBSer3 backchannel (4th port) — read-only telemetry A dedicated *machine* port that never blocks and never changes state. It speaks two command forms: - **Single characters** dispatch instantly: `A` full status, `V` ADC+current, `G` GPIO, `N` nets, `K` YAML. - **`:` verbs** read a newline-terminated line: `:gpio:s`, `:leds`, `:crossbar`, `:adc`, `:yaml`, `:every:1ms:100:gpio:s`, … Every command is gated by an access tag; only read-only queries run here, so an agent can poll the board freely without risk of mutating it. #### Self-describing `:help` (YAML) The backchannel describes itself. Send `:help` (or a bare `:`) and it emits a **YAML** document — readable straight in a serial terminal, and parseable with `yaml.safe_load()`: ```text ---YAML_HELP_START--- ser3_help: 1 protocol: single_char: "Instant dispatch; no line collection. Multi-char modifiers use ':' verbs." ... verbs: - name: "gpio:s" summary: "48-pin GPIO state snapshot" args: "[float]" output: "s{}" example: ":gpio:s" min_us: 20 ... ---YAML_HELP_END--- ``` Targeted help is available too: | Command | Returns | |---------|---------| | `:help` | Full YAML help document | | `:help:` | One verb's details (e.g. `:help:gpio:s`) | | `:help:` | A single-char command's long help (e.g. `:help:c`) | | `:help:json` | The same content in JSON | | `:cmds` / `:cmds_all` | Machine command list (JSON) — `:cmds_all` includes blocked commands and their access tags | A host harness for the backchannel ships with the firmware repo at `scripts/bench_backchannel.py` (drives `:bench`, times verbs). --- ## MCP server [**Jumperless-mcp** by LesbianVelociraptor](https://github.com/LesbianVelociraptor/jumperless-mcp) For MCP-native clients (Claude Desktop, Cursor MCP, …), the Jumperless exposes a Model Context Protocol server: a Rust MCP front-end plus a resident Python library on the device. It presents the board's capabilities as typed tools (connections, power, measurement, GPIO/PWM, OLED, overlays, state, …). It lives as the **`mcp/` git submodule** inside the canonical automation repo, so one checkout gives you both the Agent Skill and the MCP server: ```bash git clone --recurse-submodules https://github.com/Architeuthis-Flux/Large-Breadboard-Model.git cd Large-Breadboard-Model/mcp cargo install --path . # or use a release binary ``` Point your MCP client at the built binary: ```json { "mcpServers": { "jumperless": { "command": "/path/to/Large-Breadboard-Model/mcp/target/release/jumperless-mcp" } } } ``` > **Exclusive port:** the MCP server holds the USB serial port while running. > Don't run `jumperless.py` (the Agent Skill transport) against the same board > at the same time. --- ## Agent Skill If your agent supports the [Agent Skills](https://agentskills.io/) standard (Cursor, Claude Code), install the Jumperless V5 skill. It teaches the agent the device's capabilities and drives it through `scripts/jumperless.py` over the Raw REPL, with progressive-disclosure reference docs loaded only as needed. - **On this site:** [Jumperless V5 Agent Skill](jumperless-agent-skill.md) - **Canonical source:** [Architeuthis-Flux/Large-Breadboard-Model](https://github.com/Architeuthis-Flux/Large-Breadboard-Model) ```bash mkdir -p ~/.cursor/skills # or ~/.claude/skills cd ~/.cursor/skills git clone --recurse-submodules \ https://github.com/Architeuthis-Flux/Large-Breadboard-Model.git jumperless-v5 ``` --- ## Node Map This file documents the numeric IDs and common names for all key nodes on the Jumperless V5, based on `modules/jumperless/modjumperless.c`. Use it to translate between human‑readable names (like `D13` or `TOP_RAIL`) and the internal node numbers. All constants are available in the MicroPython REPL; you normally do **not** need the numbers directly, but the mapping is useful for mental models and debugging. ### Breadboard rows (1–60) Rows 1–60 represent the breadboard rows along the main area: - `1`–`30` — upper half - `31`–`60` — lower half Each row corresponds to a vertical strip of 5 holes on the physical board. Example: ```python connect(1, 10) # connect top row 1 to top row 10 connect(35, GND) # connect lower half row 35 to ground ``` --- ### DACs and ADCs #### Special nodes and supplies | Name | Aliases | ID | Notes | |---------------------|--------------------------------------|-----|-----------------------------------------| | `GND` | `GROUND` | 100 | Global ground net | | `TOP_RAIL` | `T_R`, `TOP_R` | 101 | Adjustable top rail | | `BOTTOM_RAIL` | `BOT_RAIL`, `B_R` | 102 | Adjustable bottom rail | | `DAC0` | `DAC_0` | 106 | DAC channel 0 `*` | | `DAC1` | `DAC_1` | 107 | DAC channel 1 | > [!CAUTION] > `*` Avoid using **DAC 0** and **ROUTABLE_BUFFER_IN** because they're used for probe switch sensing #### Current sense | Name | Aliases | ID | |-----------------|-------------------------------------------|-----| | `ISENSE_PLUS` | `ISENSE_POS`, `ISENSE_P`, `INA_P`, `I_P`, `CURRENT_SENSE_PLUS`, `I_POS` | 108 | | `ISENSE_MINUS` | `ISENSE_NEG`, `ISENSE_N`, `INA_N`, `I_N`, `CURRENT_SENSE_MINUS`, `I_NEG` | 109 | These two nodes are the ends of a **2 Ω shunt resistor** and are internally shorted through that shunt; always use them in series with the circuit being measured. #### ADC nodes | Name | Aliases | ID | Notes | |---------|----------------|-----|------------------------------| | `ADC0` | `ADC_0`, `ADC0_8V` | 110 | ADC channel 0 (±8 V) | | `ADC1` | `ADC_1`, `ADC1_8V` | 111 | ADC channel 1 (±8 V) | | `ADC2` | `ADC_2`, `ADC2_8V` | 112 | ADC channel 2 (±8 V) | | `ADC3` | `ADC_3`, `ADC3_8V` | 113 | ADC channel 3 (±8 V) | | `ADC4` | `ADC_4`, `ADC4_5V` | 114 | ADC channel 4 (5 V range) | | `ADC7` | `ADC_7`, `ADC7_PROBE`, `PROBE` | 115 | Probe ADC channel | --- ### User GPIO pins These are the main user‑accessible GPIOs. | Name | Aliases | ID | RP2350B Pin | Notes | |--------------|----------------------------------|-----|------------|-------------------| | `GPIO_1` | `RP_GPIO_1`, `GPIO1`, `GP_1`, `GP1` | 131 | 20 | I'm sorry they're 1-indexed like this | `GPIO_2` | `RP_GPIO_2`, `GPIO2`, `GP_2`, `GP2` | 132 | 21 | | `GPIO_3` | `RP_GPIO_3`, `GPIO3`, `GP_3`, `GP3` | 133 | 22 | | `GPIO_4` | `RP_GPIO_4`, `GPIO4`, `GP_4`, `GP4` | 134 | 23 | | `GPIO_5` | `RP_GPIO_5`, `GPIO5`, `GP_5`, `GP5` | 135 | 24 | | `GPIO_6` | `RP_GPIO_6`, `GPIO6`, `GP_6`, `GP6` | 136 | 25 | | `GPIO_7` | `RP_GPIO_7`, `GPIO7`, `GP_7`, `GP7` | 137 | 25 | | `GPIO_8` | `RP_GPIO_8`, `GPIO8`, `GP_8`, `GP8` | 138 | 27 | | `UART_TX` | `RP_UART_TX`, `TX` | 116 | 0 | 2nd serial port passthrough | `UART_RX` | `RP_UART_RX`, `RX` | 117 | 1 | When working at the MicroPython level you will usually use `GPIO_1`–`GPIO_8` and the higher‑level GPIO APIs. --- ### Buffer nodes These correspond to a routable buffer block: | Name | Aliases | ID | |---------------------|-------------------------|-----| | `ROUTABLE_BUFFER_IN` | `BUFFER_IN`, `BUF_IN`, `BUFF_IN`, `BUFFIN` | 139 | | `ROUTABLE_BUFFER_OUT` | `BUFFER_OUT`, `BUF_OUT`, `BUFF_OUT`, `BUFFOUT` | 140 | > [!CAUTION] > `*` Avoid using **DAC 0** and **ROUTABLE_BUFFER_IN** because they're used for probe switch sensing --- ### Arduino Nano header The Nano header pins are mapped as follows: | Name | Aliases | ID | Routable? | Notes | |---------------|----------|-----|-----------|-------| | `NANO_VIN` | `VIN` | 69 |N | `NANO_D0` | `D0` | 70 |Y | `NANO_D1` | `D1` | 71 |Y | `NANO_D2` | `D2` | 72 |Y | `NANO_D3` | `D3` | 73 |Y | `NANO_D4` | `D4` | 74 |Y | `NANO_D5` | `D5` | 75 |Y | `NANO_D6` | `D6` | 76 |Y | `NANO_D7` | `D7` | 77 |Y | `NANO_D9` | `D9` | 79 |Y | `NANO_D10` | `D10` | 80 |Y | `NANO_D11` | `D11` | 81 |Y | `NANO_D12` | `D12` | 82 |Y | `NANO_D13` | `D13` | 83 |Y | `NANO_AREF` | `AREF` | 85 |Y | `NANO_A0` | `A0` | 86 |Y | `NANO_A1` | `A1` | 87 |Y | `NANO_A2` | `A2` | 88 |Y | `NANO_A3` | `A3` | 89 |Y | `NANO_A4` | `A4` | 90 |Y | `NANO_A5` | `A5` | 91 |Y | `NANO_A6` | `A6` | 92 |Y | `NANO_A7` | `A7` | 93 |Y | `NANO_RESET_0`| `RST0` | 94 |N|Wired directly to RP2350B pin 18 | `NANO_RESET_1`| `RST1` | 95 |N|PSRAM Mod kits use this as CS (pin 19) | `NANO_GND_1` | `N_GND1` | 96 |N | `NANO_GND_0` | `N_GND0` | 97 |N | `NANO_3V3` | | 98 |N | `NANO_5V` | | 99 |N These nodes let you route signals between the RP2350, the breadboard, and any compatible Nano‑footprint dev board (classic Nano, Nano ESP32, Nano RP2040 Connect, etc.). --- ### Rail pads Special nodes representing where the probe can tap the rails: | Name | Aliases | ID | |------------------|--------------------------|-----| | `TOP_RAIL` | `TOP_RAIL_PAD` | 101 | | `BOTTOM_RAIL` | `BOTTOM_RAIL_PAD` | 102 | | `TOP_RAIL_GND` | `TOP_GND_PAD` | 104 | | `BOTTOM_RAIL_GND`| `BOT_RAIL_GND`, `BOTTOM_GND_PAD` | 126 | --- ### Breadboard layout (high‑level) The board follows a standard half‑size breadboard geometry: - Nano header at the top: - Top row: `D12 D11 D10 D9 D8 D7 D6 D5 D4 D3 D2 GND RST1 D0 D1` - Second row: `D13 3V3 REF A0 A1 A2 A3 A4 A5 A6 A7 5V RST0 GND VIN` - Top power rail: - `TOP_RAIL` - `GND` - Breadboard rows: - Rows `1`–`30` — upper half, between the top rails and the center gap - Rows `31`–`60` — lower half, between the center gap and the bottom rails - Bottom power rail: - `BOTTOM_RAIL` - `GND` An ASCII diagram (from the docs) approximates the layout; the agent should treat rows and rails as in any typical solderless breadboard, with the addition that every hole has an individually controllable RGB LED. #### DIP ICs straddling the center gap A DIP chip (555, op-amp, logic, …) sits across the center gap, with each side in a different breadboard half. The two halves are independent rows, so the two sides of the chip map to numerically distant nodes. For a chip whose first pin sits at upper-half row `N`, the upper-side pins run `N, N+1, …` and the lower-side pins run from the row directly across the gap (`N+30`). Worked example — an 8-pin DIP (e.g. a 555) with pin 1 at row 1: | Pin | Node | Pin | Node | |-----|------|-----|------| | 1 | `1` | 8 | `31` | | 2 | `2` | 7 | `32` | | 3 | `3` | 6 | `33` | | 4 | `4` | 5 | `34` | Pins 1–4 are on the upper side (rows 1–4); pins 5–8 are across the gap on the lower side (rows 34→31, counting back toward pin 1). Adjust the base row to wherever the chip is actually placed; verify with the probe or a measurement when unsure. --- #### How to use this map - Prefer named constants like `D13`, `TOP_RAIL`, `GPIO_1`, `ADC0`, `ISENSE_PLUS` when generating code. - Use the numeric IDs only when inspecting low‑level debugging output or when matching numeric IDs in `get_state()` / `get_net_info()` results. - When the user describes physical positions (“top rail”, “bottom left rail”, “row 25”), translate them into these node names/IDs before constructing `connect(...)` commands. ## JSON State Format The function `get_state()` returns a **JSON string** describing the entire board state: power rails, nets, GPIOs, and overlays. `set_state(json_str, clear_first=True, from_wokwi=False)` applies such a state. > [!NOTE] > The entire immediate state of the board is defined in this JSON (or YAML) slot file and can be shared to another Jumperless, saved as a backups, included in tutorials, etc. This file explains the structure so agents can safely inspect, modify, and re‑apply state. --- ### Top-level structure A typical `get_state()` JSON (pretty‑printed) looks like: ```json { "power": { "top_rail": 3.8, "bottom_rail": 3.8, "dac0": 3.25, "dac1": 0.0 }, "nets": [ { "index": 1, "name": "GND", "nodes": ["GND", "21"], "special": "none" }, { "index": 2, "name": "Top Rail", "nodes": ["TOP_R", "23"], "special": "RAIL", "voltage": 3.8 } // ... ], "gpio": [ { "pin": 1, "net": null, "function": "SIO", "direction": "INPUT", "pull": "down", "reading": "unknown", "floating_read": 1 }, { "pin": "TX", "net": 7, "function": "UART", "direction": "OUTPUT", "pull": "down", "reading": "unknown", "floating_read": 0 } ], "overlays": [ { "name": "border", "row": 1, "col": 1, "width": 30, "height": 10, "colors": ["003333", "003333", "..."] } ] } ``` There may be additional keys in future firmware versions; agents should **not** assume the set of keys is fixed and should preserve unknown keys when round‑tripping. --- ### `power` section ```json "power": { "top_rail": 3.8, "bottom_rail": 3.8, "dac0": 3.25, "dac1": 0.0 } ``` Fields: - `top_rail` — voltage (float) configured for `TOP_RAIL` - `bottom_rail` — voltage configured for `BOTTOM_RAIL` - `dac0`, `dac1` — DAC channel setpoints When using `set_state`, changing these values is equivalent to calling `dac_set()` on the corresponding channels. --- ### `nets` section Each item describes a net (set of nodes that are electrically connected inside the crossbar). Example: ```json { "index": 4, "name": "DAC 0", "nodes": ["DAC_0", "BUF_IN"], "special": "DAC", "voltage": 3.25 } ``` Fields: - `index` — numeric net index - `name` — human‑readable net name (may be auto‑generated or user‑defined) - `nodes` — array of node names (strings), such as `"GND"`, `"TOP_R"`, `"21"`, `"D13"`, etc. - These names correspond to the mapping in `reference/node-map.md`. - `special` — optional annotation for special nets; typical values: - `"none"` — normal net - `"RAIL"` — power rail (top or bottom) - `"DAC"` — DAC output net - `"UART_TX"` / `"UART_RX"` — UART nets - Other firmware‑defined markers - `voltage` — optional; expected or configured voltage on this net (when known) - Other fields (like `color`, `anim`, etc.) may be present and should be preserved. **Important:** `nets` describes **logical groupings** of nodes. It is **not** a direct list of bridges; bridges are derived from the crossbar configuration internally. --- ### `gpio` section Each entry describes one logical GPIO: ```json { "pin": 1, "net": null, "function": "SIO", "direction": "INPUT", "pull": "down", "reading": "unknown", "floating_read": 1 } ``` Fields: - `pin` — either: - An integer `1`–`8` for `GPIO_1`–`GPIO_8` - `"TX"` or `"RX"` for the UART pins - `net` — net index this pin is connected to (or `null` if unconnected) - `function` — textual description of function: - `"SIO"` — standard GPIO - `"UART"` — UART TX/RX - Other firmware‑defined roles - `direction` — `"INPUT"` or `"OUTPUT"` - `pull` — `"up"`, `"down"`, or `"none"` - `reading` — textual summary of the last known logic state (`"high"`, `"low"`, `"unknown"`, etc.) - `floating_read` — boolean (0/1) indicating whether floating detection is enabled Agents can **inspect** this section to understand logical GPIO usage, but should generally **change GPIO configuration via the MicroPython APIs** (`gpio_set_dir`, `gpio_set_pull`, etc.) rather than editing this JSON directly. --- ### `overlays` section Overlays describe additional graphics drawn on the breadboard RGB LEDs. They are rendered **on top of** the firmware’s net coloring. Example: ```json { "name": "border", "row": 1, "col": 1, "width": 30, "height": 10, "colors": [ "003333", "003333", "...", "003333", "000000", "...", "... more hex color strings ..." ] } ``` Fields: - `name` — overlay name (string) - `row` — top row (1–10) - `col` — left column (1–30) - `width` — width in columns - `height` — height in rows - `colors` — flat array of **hex color strings without `0x` prefix**, e.g. `"003333"`, `"FF2020"` - Length = `width * height` - Order is row‑major: top row left→right, then next row down, etc. - `"000000"` means “transparent” — do not override underlying LED color Brightness recommendation: - Avoid full‑scale white `FFFFFF`. - Prefer saturated colors with reduced magnitude (~25%): e.g. `2020FF`, `20FF20`, `FF2020`, `404040`. When editing overlays via JSON: - Always maintain the correct `width * height` number of color entries. - Preserve unknown fields if present. - Ensure that `row`, `col`, `width`, and `height` keep the overlay within the 5×60 canvas (rows 1–10, columns 1–30). Often it is easier to use the MicroPython overlay APIs (`overlay_set`, `overlay_clear`, etc.) directly instead of editing `overlays` in the state JSON manually. The JSON is still useful for inspection and for bulk export/import of complex scenes. --- ### Editing state safely General guidelines: 1. **Fetch once, apply locally.** - Call `get_state()` and parse the JSON in the host environment. - Keep this as your “baseline” snapshot. 2. **Work on a copy.** - Clone the baseline object before modifying it. - Preserve unknown fields. 3. **Modify only what you care about.** - For power: update `power.top_rail`, `power.dac0`, etc. - For overlays: add or replace specific entries in `overlays`. - Avoid rewriting `nets` unless you fully understand the implications; it is often better to use `connect()`/`disconnect()` APIs to modify connectivity. 4. **Re‑serialize and apply.** - Use a standard JSON encoder. - Call `set_state(json_string, clear_first=True)` when applying a complete, authoritative state. - If you are only adding overlays or adjusting power but want to preserve dynamic routing changes, you may choose `clear_first=False` (if supported and appropriate) or modify only overlays via MicroPython functions. 5. **Rollback on problems.** - If `set_state` leads to unexpected behavior, re‑apply the saved baseline snapshot. --- ### Example: adjusting top rail voltage via state Instead of: ```python dac_set(TOP_RAIL, 5.0) ``` You can do: ```python import json state = json.loads(get_state()) state["power"]["top_rail"] = 5.0 set_state(json.dumps(state)) ``` This approach is useful when batching multiple changes (e.g. updating both rail voltages and overlays) in a single atomic operation. --- ### Example: augmenting overlays Suppose `get_state()` returns a state with existing overlays, and you want to add a new “place‑here” marker at row 3, column 10: ```python import json state = json.loads(get_state()) # Create a 1×1 overlay with a bright but not full‑scale green place_here = { "name": "place_here_row3_col10", "row": 3, "col": 10, "width": 1, "height": 1, "colors": ["20FF20"] } state.setdefault("overlays", []) state["overlays"] = [ o for o in state["overlays"] if o.get("name") != place_here["name"] ] + [place_here] set_state(json.dumps(state)) ``` This pattern: - Preserves existing overlays. - Replaces any prior overlay with the same name. - Leaves other state fields untouched. --- ### Wokwi import (`from_wokwi=True`) When `from_wokwi=True`, `set_state()` expects `json_str` to be a Wokwi `diagram.json` rather than a Jumperless state JSON. The firmware converts the Wokwi representation into bridges, nets, and colors. Agents can: - Ask the user to paste a Wokwi JSON. - Pass it directly to `set_state(wokwi_json, clear_first=True, from_wokwi=True)`. After that, a subsequent `get_state()` will return the standard Jumperless state JSON. --- ### Robustness tips for agents - Treat `get_state()` as **authoritative** for the current internal configuration, but always cross‑check against the user’s description of external wiring and components. - Always preserve unknown keys when editing state; future firmware versions may add fields. - Prefer using high‑level MicroPython APIs (`connect`, `disconnect`, `dac_set`, `overlay_set`, etc.) for incremental changes, and use state JSON editing for **bulk** or **offline** transformations (import/export, heavy refactoring, or pre‑computed overlays). ### MicroPython tool reference (subset) The complete, authoritative list is the [MicroPython API Reference](09.5-micropythonAPIreference.md). The most common calls for automation: | Category | Key tools | |----------|-----------| | Connections | `connect()`, `disconnect()`, `nodes_clear()`, `is_connected()`, `fast_connect()` | | Slots | `nodes_save(slot)`, `switch_slot(slot)`, `get_current_slot()` | | State | `get_state()`, `set_state(json, clear_first=True)` | | Voltage | `dac_set(ch, v)`, `dac_get(ch)` | | Measurement | `adc_get(ch)`, `ina_get_current(s)`, `ina_get_voltage(s)`, `ina_get_power(s)` | | GPIO / PWM | `gpio_set()`, `gpio_get()`, `gpio_set_dir()`, `gpio_set_pull()`, `pwm()`, `pwm_stop()` | | WaveGen | `wavegen_set_wave()`, `wavegen_set_freq()`, `wavegen_set_amplitude()`, `wavegen_start()` | | UI | `oled_print()`, `oled_clear()`, `probe_read_blocking()`, `probe_button()`, `clickwheel_get_button()` | | LED overlays | `overlay_set()`, `overlay_set_pixel()`, `overlay_clear()`, `overlay_clear_all()` | | Net inspection | `get_net_info()`, `get_num_nets()`, `print_nets()`, `print_bridges()` | #### Node addressing (quick form) - **Breadboard rows:** `1`–`60` - **Rails / supplies:** `TOP_RAIL`, `BOTTOM_RAIL`, `GND`, `DAC0`, `DAC1` - **Arduino:** `D0`–`D13`, `A0`–`A7`, `AREF`, `RESET` - **GPIO:** `GPIO_1`–`GPIO_8`, `UART_TX`, `UART_RX` - **Measurement:** `ADC0`–`ADC3`, `ISENSE_PLUS`, `ISENSE_MINUS` --- ### A mental model for LLM agents The Jumperless prevents internal shorts, but **cannot see external wires or components** physically on the breadboard. An agent should maintain a model of what it believes is on the board and raise confidence through measurement and user confirmation. A lightweight per-component record works well: ```json { "id": "comp_001", "type": "resistor", "value": 1000, "pins": [5, 10], "confidence": 0.95, "detection_method": "measured", "notes": "Measured 987Ω between rows 5-10" } ``` | Confidence | Source | Meaning | |-----------|--------|---------| | 1.0 | `measured` | Electrically verified | | 0.9 | `user_confirmed` | User explicitly confirmed | | 0.7 | `user_stated` | User mentioned it casually | | 0.5 | `inferred` | Guessed from context | | 0.3 | `assumed` | Default assumption | --- ### Workflows **Safe power-up.** Before energizing an unknown board, ask what's on it, build a low-confidence model, then verify key rows with an ADC before connecting power — a hidden user wire between two rows can turn `TOP_RAIL` + `GND` into a dead short. **Identify with the probe.** Use `probe_read_blocking()` to let the user point at a node, then route an ADC to it and measure. **Characterize as a black box.** Drive a node with the WaveGen / DAC and sweep while reading an ADC. **Batch edits via state.** Fetch `get_state()`, mutate the JSON in Python, and re-apply with `set_state()` — one atomic round-trip instead of many calls. **Undo via a scratch slot.** Before destructive ops like `nodes_clear()`, save to an unused slot (e.g. `nodes_save(7)`); restore with `switch_slot(7)`. --- ### Safety guidelines 1. **Voltage range.** ADC0–ADC3 are buffered for ±8V; the board is ±9V tolerant overall. 2. **Short protection.** The firmware refuses dangerous internal connections (e.g. `TOP_RAIL` directly to `GND`), but cannot see external wiring. 3. **Confirm power.** The board is USB-powered (no barrel jack); ask if unsure. 4. **Crossbar resistance.** ~ 20–40Ω per default `connect()` path (~ 80Ω with a single path). Measure voltage *at the destination* to compensate for drop. 5. **Settling time.** Allow a few ms after routing/DAC changes (50–100 ms after `nodes_clear()` / `set_state()`) before measuring. --- ### Implementation status | Feature | Status | |---------|--------| | `connect()`, `disconnect()`, `is_connected()` | Implemented | | `dac_set()`, `adc_get()`, `ina_*()` | Implemented | | `gpio_*()`, `pwm()` | Implemented | | `oled_print()`, `probe_*()` | Implemented | | WaveGen tools | Implemented | | Slot management | Implemented | | `get_state()` / `set_state()` snapshot | Implemented | | `adc_get_stats()` (averaged sampling) | Available via the MCP server | | Slot backup for undo | Implemented (`nodes_save(7)` before destructive ops) | | USBSer3 `:help` self-describing backchannel | Implemented (firmware) | --- # File Manager The Jumperless has a built in File Manager which you can access in the menu with `/`, or enter `U` in the menu and Jumperless will mount as a USB Mass Storage drive called `JUMPERLESS` where you can edit files on the filesystem. ## File System Structure ``` ├── config.txt │ ├── slots/ │ ├── slot0.yaml │ ├── slot1.yaml │ ├── slot2.yaml │ └── ... (up to slot7.yaml) │ └── python_scripts/ ├── history.txt ├── cool_micropython_script.py ├── ... (your python scripts go here) │ ├── lib/ │ ├── jumperless.py │ └── oledgui.py │ └── examples/ ├── adc_basics.py ├── dac_basics.py ├── file_io_basics.py ├── gpio_basics.py ├── interaction_demo.py ├── pin_irq_basics.py ├── led_brightness_control.py ├── node_connections.py ├── oscilloscope.py ├── stylophone.py ├── uart_basics.py ├── uart_loopback.py ├── voltage_monitor.py └── ... (and more) ``` Each slot's configuration is stored as a YAML file in the `/slots/` directory, and the global hardware configuration is in `/config.txt`. --- ## Navigation ### Basic Movement | Control | Action | |---------|--------| | **↑/↓ Arrow Keys** or **Rotary Encoder** | Move selection up/down | | **Enter** or **Click Encoder** | Open directory or edit file (slot files get *loaded* instead - use `e` to edit them) | | **/** | Go to root directory | | **.** or **Esc** | Go up one directory| | **q** or **CTRL + q** | Quit File Manager (Ctrl+Q also quits the Text Editor) --- ### File Manager Commands | Key | Action | Description | |-----|--------|-------------| | [enter] | Open | Open file or enter directory (slot files get loaded, `.py` files run from the click menu) | | **h** | Help | Show help | | **v** | Quick view | View file contents | | **.** | Up dir | Go up one directory | | **n** | New file | Create new file (prompts for filename) | | **d** | New directory | Create new directory | | **x** | Delete | Delete file or directory (confirm with `y`/`N`) | --- ### File Type Icons and Colors | Icon | File Type | Extensions | Color | |------|-----------|------------|-------| | **⌘** | Directories | - | Cyan | | **𓆚** | Python files | .py, .pyw, .pyi | Green | | **⍺** | Text files | .txt, .md | Yellow | | **⚙** | Config files | .cfg, .conf, config.txt | Magenta | | **⟐** | JSON files | .json | Blue | | **☊** | Legacy slot files | nodeFileSlot*.txt | Orange | | **⎃** | Net color files | netColorsSlot*.txt | Pink | Images, audio, video, documents, and archives get their own icons too. Anything unrecognized (currently including the `/slots/*.yaml` slot files) shows as a grey **⍺**. --- ## Jumperless eKilo Text Editor The File Manager also has text editor based off [**eKilo**](https://github.com/antonio-foti/ekilo) ### Editor Controls - **Ctrl+S**: Save file - **Ctrl+Q**: Quit editor - **Ctrl+P**: Save and launch MicroPython REPL - **Arrow keys**: Navigate cursor - **Rotary encoder**: Move cursor horizontally - **Click encoder**: Enter character selection mode (you can scroll through the letters on the OLED and click again to insert it) ### Character Selection With the Click Wheel and OLED When using the rotary encoder in the editor: - **Click encoder**: Enter character selection mode - **Rotate encoder**: Cycle through available characters - **Click encoder**: Confirm character selection - **Wait 5 seconds**: Exit character selection mode Yes, you could write code with just the click wheel and the OLED if you really wanted to. [Image: 1760676653009] --- ## OLED Display Support If you have an OLED connected, the File Manager shows: - **Current path** and **selected file** - **File navigation** with scrolling support - **Real-time updates** as you navigate --- ### MicroPython Examples The File Manager automatically creates example Python scripts in `/python_scripts/examples/`: #### Basic Hardware Examples - [**adc_basics.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/adc_basics.py): Basic ADC (Analog-to-Digital Converter) operations. - This example shows how to read analog voltages from all ADC channels (0-3). Connect voltage sources to ADC inputs and monitor readings in real-time. - [**dac_basics.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/dac_basics.py): Basic DAC (Digital-to-Analog Converter) operations. - Shows how to set DAC voltages on all channels (DAC_A, DAC_B, TOP_RAIL, BOTTOM_RAIL). - Hardware setup: Connect voltmeter or LED to DAC output pins. - [**gpio_basics.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/gpio_basics.py): Basic GPIO (General Purpose Input/Output) operations. - This example demonstrates digital I/O, direction control, and pull resistors. - Tests input mode with pull-up, pull-down, and floating configurations. - [**file_io_basics.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/file_io_basics.py): Guided tour of file I/O with the `jfs` module. - Walks through writing, append-mode data logging, reading with seek/tell, directory operations, and the open-file limit - narrated as it runs. - [**pin_irq_basics.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/pin_irq_basics.py): `machine.Pin` interrupt basics. - Routes GPIO_1 to GPIO_2 through the crossbar and catches self-generated edges with an IRQ handler - no external wiring needed. - [**node_connections.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/node_connections.py): Node connection and routing operations. - This example shows how to connect/disconnect nodes, check connections, and clear all connections. - Demonstrates working with breadboard nodes, DAC outputs, and GPIO pins. - - [**uart_loopback.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/uart_loopback.py): UART Loopback Demo. - Demonstrates UART communication by looping back data from UART_TX to UART_RX. - Open a serial monitor on the Jumperless's second port at 115200 baud to see the looped messages. #### Interactive Examples - [**interaction_demo.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/interaction_demo.py): Interactive Demo - Control connections with probe, encoder, and buttons. - This example shows how to use all the interactive controls together. - No special hardware needed - use the probe to tap nodes, the encoder to adjust bridge spread, and buttons to change colors. - [**led_brightness_control.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/led_brightness_control.py): LED Brightness Control Demo. - Tap breadboard pads 1-60 to control the voltage on an LED and display the current draw. - Hardware setup: Connect LED anode to breadboard row 15, connect LED cathode to GND. - Displays voltage and current on OLED. - [**stylophone.py**](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/ex/stylophone.py): Jumperless Stylophone. - Musical instrument using probe and GPIO to generate audio tones. - Hardware setup: Connect speaker between rows 25 (positive) and 55 (negative). - Touch breadboard pads to play different frequencies, use probe buttons to adjust sustain. These are just the highlights - the firmware ships more examples than listed here (OLED demos, pin-interrupt games, an oscilloscope, a Google Sheets logger, and more). You can trigger them to regenerate if you messed them up by deleting it with `x` (when you're in the File Manager), and then entering `m` to create new copies of any examples it doesn't see. ## Editing Slot Files Slot files (located in `/slots/`) use **YAML format** and can be edited directly! They're human-readable files containing: - **bridges** - Your circuit connections - **power** - Rail and DAC voltages - **colors** - Wire colors from Wokwi or custom colors - **config** - Routing preferences and GPIO settings **Example slot file:** ```yaml version: 2 sourceOfTruth: bridges bridges: - {n1: 1, n2: 10, dup: 2, color: red} - {n1: NANO_D5, n2: GP_1, dup: 2} - {n1: TOP_RAIL, n2: 5, dup: 2} power: topRail: 3.30 bottomRail: 2.50 dac0: 3.33 dac1: 0.00 ``` **Named nodes** you can use: `NANO_D0-D13`, `NANO_A0-A7`, `GP_1-8` (or `RP_GPIO_1-8`), `TOP_RAIL`, `BOTTOM_RAIL`, `GND`, `DAC0`, `DAC1`, and more (see [glossary](99-glossary.md)). Note these differ from the MicroPython constants - `GPIO_1` won't parse in slot files. If you edit the active slot's file, the Jumperless automatically reloads it - when you quit the onboard eKilo editor, or, if you have the Jumperless mounted as a USB Mass Storage drive and are editing the files on your computer, when you eject/unmount the drive. --- ## USB Mass Storage Enter `U` in the menu and Jumperless will mount as a USB Mass Storage drive called `JUMPERLESS` where you can edit files on the filesystem. Keep in mind that file operations are pretty slow, so make sure to give it time to fully save files when you drop them onto the filesystem. When you're finished `u` (or just eject the drive) will unmount the Mass Storage device. You can also enter `Z` for a little debug menu --- ## Navigation Reference | Key | Action | |-----|--------| | ↑/↓ | Move selection | | Enter | Open/Edit | | / | Go to root | | . | Go up directory | | h | Show help | --- ### File Operations | Key | Action | |-----|--------| | v | View file | | e | Edit file | | i | File info | | n | New file | | d | New directory | | x | Delete | | r | Refresh | --- ### System | Key | Action | |-----|--------| | u | Memory status | | m | Initialize examples | | q or Ctrl+Q | Quit | --- # MicroPython This guide covers how to write, load, and run Python scripts that control Jumperless hardware using the embedded MicroPython interpreter. If you just want an overview of all the available calls, check out the [**MicroPython API Reference**](09.5-micropythonAPIreference.md) For stuff that's not Jumperless-specific, check out the [MicroPython Docs](https://docs.micropython.org/en/latest/index.html) ## Now you can live code with [JumperIDE](https://ide.jumperless.org/)! Holy shit I should have done this years ago Seriously, this is *such* a better experience than using the onboard text editor and REPL, you should play with it right now Go to [https://ide.jumperless.org/](https://ide.jumperless.org/) and press the connect button. Choose the 3rd Jumperless port in that list (Windows may not put them in order, so if nothing happens, try the other ones) and click Connect Then open some examples (this update should overwrite the examples with the new ones) and hit the Run / Stop button Press it again to Stop. If you make changes, hit the green Save button next to it (it takes a second and the script should be stopped.) ### If you write something cool, send it to me and I'll add it to the default examples (I'll put a page on this site soon where you can share them.) This is using [MicroPython's built-in Raw REPL](https://docs.micropython.org/en/latest/reference/repl.html#raw-mode-and-raw-paste-mode), so anything that can interact with that will work here. I've only tested with Viper IDE but I'm pretty sure just about anything else would work. There's also `jumperless.py` and `jumperless.pyi` module with stubs for all the built-in functions so syntax highlighting and autocomplete will work in your favorite code editor (sorry, autocomplete for jumperless functions doesn't work in ViperIDE.) You can grab them here: ### [jumperless.py](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/jumperless.py) ### [jumperless.pyi](https://github.com/Architeuthis-Flux/JumperlOS/blob/main/scripts/jumperless.pyi) --- ## JumperIDE for VS Code [[Image: VSCode Marketplace]](https://marketplace.visualstudio.com/items?itemName=ArchiteuthisFlux.jumperide) [[Image: Open VSX]](https://open-vsx.org/extension/ArchiteuthisFlux/jumperide) [[Image: GitHub Release]](https://github.com/Architeuthis-Flux/JumperIDE-VSCode/releases/latest) [[Image: License: Unlicense]](https://github.com/Architeuthis-Flux/JumperIDE-VSCode/blob/main/LICENSE) --- #### Walkthrough **Connect** — the board's serial ports are auto-detected by USB ID, with the MicroPython REPL port pre-selected: [Image: Connecting to a Jumperless V5] **Serial terminal** — pick any port (port1 is the device menu), or hand the port to the standalone Jumperless App: [Image: Serial terminal port picker] **REPL + device menu side by side** — the MicroPython REPL and the board's interactive menu, each on its own port: [Image: REPL and main serial terminal side by side] **OLED bitmap editor** — draw pixels and watch them appear on the board's OLED live: [Image: Editing an OLED bitmap with live push to the device] **API reference panel** — the full MicroPython API docs beside your code: [Image: API reference panel] --- #### Install Either search `JumperIDE` in the Extensions view ([Open VSX](https://open-vsx.org/extension/ArchiteuthisFlux/jumperide)) or just download the `.vsix` from the [latest release](https://github.com/Architeuthis-Flux/JumperIDE-VSCode/releases/latest) (install command included in the release notes). #### Features ##### **Actions panel** Everything in one sidebar panel: a connection button that doubles as live status (hover to connect or disconnect), **Run/Stop**, **Save to Jumperless**, **Save Locally** (export a device file to your computer), **OLED Bitmap**, the **Serial Terminal**, and quick access to the **API reference** and **JumperNet publishing**. ##### **Serial connection** The V5 exposes four USB serial ports; the extension detects them by USB ID and pre-selects the MicroPython REPL port (the 3rd). Set `jumperless.connectOnStartup` to connect automatically. ##### **Run / Stop** `Run` executes the file in the current editor on the board, output streams to the REPL terminal. `Stop` interrupts. [Image: Connect Run Stop] ##### **Device file browser** The board's filesystem in the sidebar. Files open as local working copies (so the language server works on them); saving pushes back to the board. Files that didn't come from the board ask for a device path on first save, then remember it. Create, delete, and upload files and folders. ##### **REPL terminal** A terminal connected to the board's MicroPython prompt. Handles MicroPython line endings and batches output so fast prints don't stall the UI. ##### **Serial terminal** Pick any serial port (port1, the board's menu/CLI, is recommended) for a direct raw-passthrough terminal — the full-color menus and ANSI art render exactly as the board sends them. Or pick `Use Jumperless App` to run the standalone [Jumperless App](https://github.com/Architeuthis-Flux/Jumperless-App) instead (auto-installs from PyPI; autodetects the port and handles reconnection). ###### **Autocomplete & hover docs** Signatures and descriptions for every Jumperless function, sourced from the [API reference](https://docs.jumperless.org/09.5-micropythonAPIreference/) and refreshed automatically. Jumperless calls and constants are highlighted in Python files. ##### **OLED bitmap editor** A pixel editor for OLED `.bin` files. While connected, edits push live to the board's OLED as you draw. **Jumperless: New OLED Bitmap** creates a blank 128×32 canvas on the device or locally. ##### **JumperNet registry** Browse community scripts and OLED images, open them, save them to the board, or publish your own (**Jumperless: Publish Script to Registry**). Feel free to publish whatever work in progress scripts, you or (anyone else) can update the same script and keep version history. ##### **API reference panel** `Jumperless: Open API Reference` opens the [MicroPython API docs](https://docs.jumperless.org/09.5-micropythonAPIreference/) beside your code. #### Zero-import autocomplete On the board, scripts run with the full API preloaded (`from jumperless import *` happens before your code). The editor matches that automatically: on first activation the extension installs typed stubs and points your Python analyzer at them, so files opened from the device resolve the whole API with no imports and no setup. (Controlled by `jumperless.setup.autoSetUpGlobally`, on by default.) - `typings/jumperless.pyi` — typed stub, synced from [JumperlOS](https://github.com/Architeuthis-Flux/JumperlOS) - `typings/builtins.pyi` — standard-library builtins with Jumperless globals layered on top (typo detection still works) - `typings/time.pyi` — MicroPython `time` extras (`ticks_ms`, `sleep_ms`, …) To get the same thing in one of your own project folders (checked into that repo instead of user settings), run **Jumperless: Set Up This Folder for Jumperless Python** — it writes the `typings/` folder and a `pyrightconfig.json` into the workspace. --- ## Quick Start (Built-in REPL) From the main Jumperless menu, press `p` to enter the MicroPython REPL: [Image: Screenshot 2025-07-04 at 7 03 24 PM] ## REPL Navigation Up / Down arrow keys on a blank prompt will scroll through history, any other key will break out of history mode and enter multiline editing. So you can use arrow keys to navigate and edit the script. In history mode, the `>>>` prompts will be pink, when you're editing, they'll be blue. ## Hardware Control Functions All Jumperless hardware functions are automatically imported into the global namespace - no prefix is actually necessary, but it's probably good to use `import jumperless as j` when using Viper IDE or something so it doesn't complain about not undefined names. --- ## Basic Script Structure ```jython """ My Jumperless Script Description of what this script does """ print("Starting my script...") # Connect some nodes connect(1, 5) connect(2, 6) # Set up GPIO gpio_set_dir(1, True) # Output gpio_set_dir(2, False) # Input # Main loop for i in range(10): gpio_set(1, True) time.sleep(0.5) gpio_set(1, False) time.sleep(0.5) # Read input (gpio_get returns truthy for HIGH, falsy for LOW) if gpio_get(2): print("Button pressed!") # Cleanup nodes_clear() print("Script complete!") ``` ## Loading and Running Scripts ### Method 1 (Recommended): [Viper IDE](https://viper-ide.blackhart.dev/) See [above](#now-you-can-live-code-with-viper-ide) for instructions. It's at the top of the page for a reason, it's awesome. ### Method 2: File Manager From the REPL (enter `p` in the main menu), then type `files` to open the file manager: ```jython >>> files ``` Navigate to your script and press Enter to load it for editing, then press `Ctrl+P` to load it into the REPL for execution. **Note:** The standard Python `exec(open(...).read())` also works - the filesystem is mounted as a MicroPython VFS, so the built-in `open()` and the `os` module operate on the same files as `jfs`. ### Method 3: REPL Commands From the MicroPython REPL, you can use the following commands to manage scripts: ```jython # Load script into editor for modification load my_script.py # Save current session as script save my_new_script.py ``` ### Method 4: Direct Execution From the main Jumperless menu, you can execute single commands: ```jython > gpio_set(1, True) > adc_get(0) > connect(1, 5) ``` ## REPL (Interactive Mode) ### Starting REPL From main menu: Press `p` ### REPL Commands ```jython CTRL + q - Exit REPL history - Show command history and saved scripts save [name] - Save last executed script load - Load script by name or number files - Open file manager new - Create new script with eKilo editor helpl - Show REPL help help() - Show hardware commands ``` ### Navigation ``` ↑/↓ arrows - Browse command history ←/→ arrows - Move cursor, edit text TAB - Add 4-space indentation Enter - Execute (empty line in multiline to finish) Ctrl+Q - Force quit REPL or interrupt running script ``` ### Multiline Auto-Indent Mode The REPL automatically detects when you need multiple lines after a `:` ```jython >>> def blink_led(): ... for i in range(5): ... gpio_set(1, True) ... time.sleep(0.5) ... gpio_set(1, False) ... time.sleep(0.5) ... >>> blink_led() ``` If you want to use *real* multiline mode, use the Kilo file editor. ### Command History - Use ↑/↓ arrows to browse previous commands - Commands are automatically saved - Type `history` to see all saved scripts ## Connection Context Switching The MicroPython REPL now supports **connection contexts** that determine how connections persist: - **`global` context**: Changes persist to global state - connections remain after exiting Python - **`python` context**: Connections are restored to how they were when exiting REPL (saved to `slots/slotPython.yaml`) **To toggle contexts:** Type `context` in the REPL **How it works:** - In `global` mode: Any connections you make become permanent, just like using the normal command interface - In `python` mode: The connection state when you entered the REPL is saved, and restored when you exit - The current context is displayed in the REPL prompt ## Built-in Examples The system includes several example scripts. To run an example: 1. Type `files` in the REPL. 2. Navigate to the `examples/` directory. 3. Select the desired script and press Enter to edit/view it. 4. Press `Ctrl+P` to load it into the REPL for execution. Example scripts include: - `dac_basics.py` - `adc_basics.py` - `gpio_basics.py` - `node_connections.py` - `led_brightness_control.py` - `stylophone.py` - `uart_basics.py` - `uart_loopback.py` - `interaction_demo.py` - `test_neopixel.py` - `fake_gpio.py` **REPL not responding:** - Press Ctrl+Q to force quit - Unplug / replug your Jumperless (don't worry, almost everything is persistent) ## Formatted Output and Custom Types The Jumperless module returns custom types that print nicely but also work in conditionals: ```jython # GPIO functions return custom types that print as readable strings state = gpio_get(1) # Prints "HIGH", "LOW", or "FLOATING" direction = gpio_get_dir(1) # Prints "INPUT" or "OUTPUT" pull = gpio_get_pull(1) # Prints "PULLUP", "PULLDOWN", or "NONE" # These types are also truthy/falsy for use in conditionals: if gpio_get(1): # True if HIGH, False if LOW or FLOATING print("Pin is HIGH") if gpio_get_dir(1): # True if OUTPUT, False if INPUT print("Pin is output") # Connection status works the same way connected = is_connected(1, 5) # Prints "CONNECTED" or "DISCONNECTED" if connected: # True if connected, False if not print("Nodes are connected") # Voltage and current readings are floats voltage = adc_get(0) # Returns float (e.g., 3.300) current = ina_get_current(0) # Returns float in A (e.g., 0.0123) power = ina_get_power(0) # Returns float in W (e.g., 0.4567) # All functions work with both numbers and string aliases gpio_set_dir("GPIO_1", True) # Same as gpio_set_dir(1, True) connect("TOP_RAIL", "GPIO_1") # Same as connect(101, 131) ``` --- # MicroPython API Reference [Node Connections](#node-connections) (Connecting stuff with the Jumperless) - *[Examples](#node-connections)*: - `connect(node1, node2, [duplicates=-1])` - Connect two nodes (duplicates: -1=add, N=force N) - `disconnect(node1, node2)` - Disconnect nodes (`node2` = -1 clear everything connected to `node1`) - `fast_connect(node1, node2, [duplicates=-1])` - Connect, skip LED update - `fast_disconnect(node1, node2)` - Disconnect, skip LED update - `is_connected(node1, node2)` - Check if nodes are connected - `nodes_clear()` - Clear all connections [DAC](#dac-digital-to-analog-converter) (Digital-to-Analog Converter) - *[Examples](#dac-digital-to-analog-converter)*: - `dac_set(channel, voltage)` - Set DAC output voltage - `dac_get(channel)` - Get DAC output voltage [ADC](#adc-analog-to-digital-converter) (Analog-to-Digital Converter) - *[Examples](#adc-analog-to-digital-converter)*: - `adc_get(channel)` - Read ADC input voltage [GPIO](#gpio-general-purpose-inputoutput) (General Purpose Input / Output from the RP2350B) - *[Examples](#gpio-general-purpose-inputoutput)*: - `gpio_set(pin, value)` - Set GPIO pin state - `gpio_get(pin)` - Read GPIO pin state - `gpio_set_dir(pin, direction)` - Set GPIO pin direction - `gpio_get_dir(pin)` - Get GPIO pin direction - `gpio_set_pull(pin, pull)` - Set GPIO pull-up/down - `gpio_get_pull(pin)` - Get GPIO pull-up/down [INA](#ina-currentpower-monitor) (Current/Power Monitor) - *[Examples](#ina-currentpower-monitor)*: - `ina_get_current(sensor)` - Read current in amps - `ina_get_voltage(sensor)` - Read shunt voltage - `ina_get_bus_voltage(sensor)` - Read bus voltage - `ina_get_power(sensor)` - Read power in watts [PWM](#pwm-pulse-width-modulation) (Pulse Width Modulation) - *[Examples](#pwm-pulse-width-modulation)*: - `pwm(pin, [frequency], [duty])` - Setup PWM on GPIO pin - `pwm_set_duty_cycle(pin, duty)` - Set PWM duty cycle - `pwm_set_frequency(pin, freq)` - Set PWM frequency - `pwm_stop(pin)` - Stop PWM on pin [OLED Display](#oled-display) (Organic Light Emitting Diode 128 x 32 pixel display) - *[Examples](#oled-display)*: - `oled_print("text")` - Display text - `oled_clear()` - Clear display - `oled_connect()` - Connect OLED - `oled_disconnect()` - Disconnect OLED - `Screen()` / `Text(...)` (oledgui) - Retained, live-updating layouts ([OLED Layout / Screens](#oled-layout--screens-retained-live-updating)) [Graphic Overlays](#graphic-overlays) (Directly controlling the breadboard LEDs) - *[Examples](#graphic-overlays)*: - `overlay_set(name, row, col, w, h, colors)` - Create/update overlay - `overlay_clear(name)` - Remove overlay - `overlay_clear_all()` - Remove all overlays - `overlay_shift(name, dRow, dCol)` - Move overlay relative - `overlay_place(name, row, col)` - Move overlay absolute - `overlay_set_pixel(row, col, color)` - Set single pixel - `overlay_count()` - Get active overlay count - `overlay_serialize()` - Get overlays as YAML [Probe Functions](#probe) (Probe status / actions)=L=e==ft - *[Examples](#probe)*: - `probe_read_blocking()` - Wait for probe touch (don't return until a pad is touched) - `probe_read_nonblocking()` - Check probe immediately (return -1 if no pad is touched) - `get_button([blocking=True])` - Get button state (default: blocking) - `probe_button([blocking=True])` - Get button state (default: blocking) - `probe_button_blocking()` - Wait for button press - `probe_button_nonblocking()` - Check buttons immediately - `check_button()` - Check for probe button presses - `get_switch_position()` - Get probe switch position (0=measure, 1=select, -1=unknown) - `set_switch_position(position)` - Set switch position manually - `check_switch_position()` - Check switch via current sensing - `probe_autoconnect([enable])` - Get/set probe auto-connect (True/False, temporary until reboot) [Clickwheel](#clickwheel) (Rotary Encoder / button on the top left) - *[Examples](#clickwheel)*: - `clickwheel_get_position()` - Get raw position counter - `clickwheel_reset_position()` - Reset position to 0 - `clickwheel_get_direction([consume])` - Get direction event (NONE/UP/DOWN) - `clickwheel_get_button()` - Get button state [Fake GPIO](#fake-gpio-virtual-gpio-pins) (Multiplex an ADC to read up to 32 inputs) - *[Examples](#fake-gpio-virtual-gpio-pins)*: - `FakeGpioPin(node, [mode], [v_high], [v_low])` - Create virtual GPIO - `pin.value([val])` - Control pin [Net Information](#net-information-api) (Ask the Jumperless about `Nets`) - *[Examples](#net-information-api)*: - `get_net_name(netNum)` - Get the name of a net - `set_net_name(netNum, name)` - Set a custom net name - `get_net_color(netNum)` - Get net color as 0xRRGGBB - `get_net_color_name(netNum)` - Get net color as name string - `set_net_color(netNum, color)` - Set net color by name or RGB - `set_net_color_hsv(netNum, h, [s], [v])` - Set net color by HSV (auto-detects 0-1 or 0-255) - `get_num_nets()` - Get number of active nets - `get_num_bridges()` - Get number of bridges - `get_net_nodes(netNum)` - Get comma-separated node list - `get_bridge(bridgeIdx)` - Get bridge info tuple - `get_net_info(netNum)` - Get full net info as dict [Path Query API](#path-query-api) (Info about the internal routing) - *[Examples](#path-query-api)*: - `get_num_paths([include_duplicates])` - Get number of routing paths - `get_path_info(path_idx)` - Get detailed path info as dict - `get_all_paths()` - Get all paths as list of dicts - `get_path_between(node1, node2)` - Query specific path between nodes [Net Current Scan](#net-current-scan-api) (Board-wide passive voltage/current sensing) - *[Examples](#net-current-scan-api)*: - `get_node_voltage(node)` - Scanned node voltage (None if no data) - `get_net_current(netNum)` - Net current info dict (None if no data) - `get_path_current(path_idx)` - Signed path current in mA (None if no data) [JFS](#jfs-jumperless-filesystem) (Jumperless File System) - *[Examples](#jfs-jumperless-filesystem)*: - `jfs.open(path, mode)` - Opens file, returns file handle - `jfs.read(file, size=1024)` - Read from file - `jfs.write(file, data)` - Write to file - `jfs.close(file)` - Close file - `jfs.seek(file, position, whence=0)` - Seek in file - `jfs.tell(file)` - Get current position - `jfs.size(file)` - Get file size - `jfs.available(file)` - Get bytes available - `jfs.exists(path)` - Check if path exists (returns True/False) - `jfs.listdir(path)` - List directory contents (returns list) - `jfs.mkdir(path)` - Create directory - `jfs.rmdir(path)` - Remove directory - `jfs.remove(path)` - Remove file - `jfs.rename(from, to)` - Rename/move file - `jfs.stat(path)` - Get file/directory status info - `jfs.info()` - Returns (total, used, free) tuple - - `file.print(data)` - Print to the file (like `print()` but writes to the file, adds a newline) - - `file.flush()` - Flush buffered data to file - - `file.position()` - Alias for `f.tell()` - - `file.name()` - Get file name [Status](#status-functions) (Print status stuff to main Serial) - *[Examples](#status-functions)*: - `print_bridges()` - Print all bridges - `print_paths()` - Print path between nodes - `print_crossbars()` - Print crossbar array - `print_nets()` - Print nets - `print_chip_status()` - Print chip status [Misc / System Functions](#system-functions) (Things that don't have their own category) - *[Examples](#system-functions)*: - `arduino_reset()` - Reset Arduino - `run_app('appName')` - Run app - `pause_core2(pause)` - Pause/resume core2 processing - `send_raw(chip, x, y, setOrClear)` - Send raw data to core2 - `change_terminal_color(color, [flush])` - Set terminal color (0-255) - `cycle_term_color([reset], [step], [flush])` - Cycle through colors - `force_service(name)` - Force run a service by name - `force_service_by_index(idx)` - Force run service by index (faster) - `get_service_index(name)` - Get service index for caching - `context_toggle()` - Toggle connection context (global/python) - `context_get()` - Get current context name - `nodes_save([slot])` - Save connections to slot - `nodes_discard()` - Discard unsaved changes - `nodes_has_changes()` - Check for unsaved changes - `switch_slot(slot)` - Switch to a different slot - `get_state()` - Get complete board state as JSON string - `set_state(json, [clear_first=True], [from_wokwi=False])` - Apply complete state from JSON string; set `from_wokwi` to True to parse Wokwi diagram [Help](#help-functions) - *[Examples](#the-entire-output-of-help)*: - `help()` - Display help - `nodes_help()` - Show available nodes and how to address them --- ## A Note on Usage All functions and constants from the `jumperless` module are automatically imported into the global namespace. This means you can call them directly (e.g., `connect(1, 5)`) without needing the `jumperless.` prefix. There are three primary ways to specify nodes in functions: 1. **By Number**: Use the integer corresponding to the breadboard row (1-60). 2. **By String Name**: Use a case-insensitive string for any named node (e.g., `"d13"`, `"TOP_RAIL"`). 3. **By Constant**: Use the predefined, case-sensitive constant for a node (e.g., `D13`, `TOP_RAIL`). --- ## Node Connections These functions manage the connections between nodes on the breadboard and special function pins. ### `connect(node1, node2, [duplicates=-1])` Creates a bridge between two nodes. * `node1`, `node2`: The nodes to connect. Can be integers, strings, or constants. * `duplicates` (optional): Controls duplicate connection behavior: * `-1` (default): Just add the connection without managing duplicates (standard behavior) * `0`: Force exactly 0 duplicates (removes any existing duplicate paths) * `1+`: Force exactly N duplicates (adds or removes connections to reach that count) **Example:** ```jython # Connect breadboard row 1 to row 30 connect(1, 30) # Connect Arduino D13 to the top power rail connect(D13, TOP_RAIL) # Connect GPIO 1 to ADC 0 using strings connect("GPIO_1", "ADC0") # Duplicate management examples connect(1, 5, duplicates=0) # Ensure no duplicate paths connect(1, 5, duplicates=2) # Force exactly 2 parallel paths connect(1, 5) # Standard: just add the connection ``` ### `disconnect(node1, node2)` Removes a specific bridge between two nodes. * `node1`, `node2`: The two nodes to disconnect. * To remove all connections from a single node, set `node2` to `-1`. **Example:** ```jython # Remove the bridge between rows 1 and 30 disconnect(1, 30) # Remove all connections from GPIO_1 disconnect(GPIO_1, -1) ``` ### `is_connected(node1, node2)` Checks if a direct or indirect connection exists between two nodes. * Returns a custom `ConnectionState` object which evaluates to `True` if connected (`CONNECTED`) and `False` if not (`DISCONNECTED`). **Example:** ```jython if is_connected(D13, TOP_RAIL): print("D13 is connected to the top rail.") state = is_connected(1, 2) print(state) # Prints "CONNECTED" or "DISCONNECTED" ``` ### `nodes_clear()` Removes all connections from the board. **Example:** ```jython nodes_clear() print("All connections cleared.") ``` ### `node(name_or_id)` Creates a node object from a string name or integer ID. This is useful for storing a node reference in a variable. **Node Type:** `Node` objects support arithmetic and comparison operations with integers: * **Comparisons**: `==`, `!=`, ``, `>=` * **Arithmetic**: `+`, `-`, `*`, `//`, `%` * **Conversions**: `int(node)` converts to integer value **Example:** ```jython my_pin = node("D7") led_pin = node(15) connect(my_pin, led_pin) oled_print(my_pin) # Displays 'D7' on the OLED # Arithmetic and comparison work! if my_pin 10 MΩ sensors) and may cause a small leakage current or change the measured state. If your external circuit is super high‑impedance, either disable floating-read for that pin or add a defined pull resistor. ### `gpio_get_read_floating(pin)` Returns whether floating-read detection is enabled for a GPIO pin. * `pin`: The GPIO pin number (1-10). * Returns: `True` if floating-read is enabled, otherwise `False`. * **Aliases**: `get_gpio_read_floating()` **Example:** ```jython # Enable floating detection on GPIO 3 gpio_set_read_floating(3, True) # Confirm it is enabled print(gpio_get_read_floating(3)) # True or False # If a connected high‑impedance sensor behaves oddly, disable floating-read gpio_set_read_floating(3, False) ``` **Pinout:** * `1-8`: Routable GPIO pins `GPIO_1` to `GPIO_8`. * `9`: `UART_TX`. * `10`: `UART_RX`. **Example:** ```jython # Set GPIO 1 as an output and turn it on gpio_set_dir(1, True) gpio_set(1, True) # Set GPIO 2 as an input with a pull-up gpio_set_dir(2, False) gpio_set_pull(2, 1) # Read the state of GPIO 2 state = gpio_get(2) if state: # GPIOState is truthy when HIGH, falsy when LOW or FLOATING print("GPIO 2 is HIGH") # You can also print it directly - shows "HIGH", "LOW", or "FLOATING" print("State: " + str(state)) ``` --- ## PWM (Pulse-Width Modulation) Functions for generating PWM signals on GPIO pins. ### `pwm(pin, [frequency], [duty_cycle])` Sets up and starts a PWM signal on a GPIO pin. * `pin`: The GPIO pin to use (1-8). * `frequency` (optional): The PWM frequency in Hz (0.001 to 62500000). Defaults to 1000. * `duty_cycle` (optional): The duty cycle from 0.0 to 1.0. Defaults to 0.5. * **Aliases**: `set_pwm()` **Frequency Ranges:** * **Hardware PWM**: 10Hz to 62.5MHz (high precision, hardware peripheral) * **Slow PWM**: 0.001Hz to 10Hz (hardware timer based, precise timing) * **Automatic Selection**: System automatically chooses the appropriate mode based on frequency ### `pwm_set_duty_cycle(pin, duty_cycle)` Changes the duty cycle of an existing PWM signal. * `pin`: The GPIO pin number (1-8). * `duty_cycle`: The new duty cycle (0.0 to 1.0). * **Aliases**: `set_pwm_duty_cycle()` ### `pwm_set_frequency(pin, frequency)` Changes the frequency of an existing PWM signal. * `pin`: The GPIO pin number (1-8). * `frequency`: The new frequency in Hz (0.001 to 62500000). * **Aliases**: `set_pwm_frequency()` ### `pwm_stop(pin)` Stops the PWM signal on a GPIO pin. * `pin`: The GPIO pin number (1-8). * **Aliases**: `stop_pwm()` **Example:** ```jython # Hardware PWM: 1kHz, 25% duty cycle on GPIO_1 pwm(GPIO_1, 1000, 0.25) # Slow PWM: 0.1Hz (10 second period), 50% duty cycle on GPIO_2 pwm(GPIO_2, 0.1, 0.5) # Ultra-slow PWM: 0.001Hz (1000 second period), 25% duty cycle on GPIO_3 pwm(GPIO_3, 0.001, 0.25) # Change the duty cycle to 75% pwm_set_duty_cycle(GPIO_1, 0.75) # Change frequency (will automatically switch between hardware/slow PWM) pwm_set_frequency(GPIO_1, 500) # 500Hz (hardware PWM) pwm_set_frequency(GPIO_1, 5) # 5Hz (slow PWM) # Stop the PWM signal pwm_stop(GPIO_1) ``` --- ## Graphic Overlays Functions for creating and manipulating 2D color overlays on the breadboard LEDs. The breadboard is addressed as a **10-row × 30-column grid**: * **Row 1-5**: Top half (A-E) * **Row 6-10**: Bottom half (F-J) * **Column 1-30**: Breadboard columns 1-30 ### `overlay_set(name, x, y, height, width, colors)` Creates or updates a graphic overlay. * `name`: Unique string identifier for the overlay. * `x`: Starting column (**1-30**). Matches breadboard column labels. * `y`: Starting row (**1-10**). 1-5 = Top half (A-E), 6-10 = Bottom half (F-J). * `height`: Height in rows (y-dimension). * `width`: Width in columns (x-dimension). * `colors`: List of 32-bit integer colors (0xRRGGBB). Can be a flat list or 2D list (rows). * Returns the overlay index. **Note:** If you pass a 2D array (list of lists) for `colors`, the outer list represents rows (height) and the inner lists represent columns (width). Make sure your `width` and `height` arguments match the data shape. **Example:** ```jython # Create a 5-wide, 2-high overlay at x=2, y=5 # x=2 (Column 2), y=5 (Row 5/E) colors_2d = [ [0x550000, 0x550000, 0x550000, 0x550000, 0x550000], # Row 0 [0x550000, 0x550000, 0x550000, 0x550000, 0x550000] # Row 1 ] overlay_set("box_2d", 12, 5, 2, 5, colors_2d) ``` ### `overlay_clear(name)` Removes a specific overlay. * `name`: The identifier of the overlay to remove. * Returns `1` if found/removed, `0` otherwise. ### `overlay_clear_all()` Removes ALL active overlays. ### `overlay_shift(name, dx, dy)` Moves an overlay by a relative offset. Wraps around edges. * `name`: Overlay identifier. * `dx`: Column delta (e.g., 1 for right, -1 for left). * `dy`: Row delta (e.g., 1 for down, -1 for up). * Returns `1` if found, `0` otherwise. ### `overlay_place(name, x, y)` Moves an overlay to a specific absolute position. Wraps around edges. * `name`: Overlay identifier. * `x`: New column (**1-30**). * `y`: New row (**1-10**). * Returns `1` if found, `0` otherwise. **Example:** ```jython # Use the probe to move the box overlay import jumperless as j import time colors_2d = [ [0x550000, 0x104000, 0x005500, 0x001040, 0x000055], # Row 0 [0x550000, 0x104000, 0x005500, 0x001040, 0x000055], # Row 1 ] j.overlay_set("box_2d", 12, 5, 2, 5, colors_2d) while True: node = j.probe_read_blocking() j.overlay_place("box_2d",node, 5) time.sleep(0.1) ``` ### `overlay_set_pixel(x, y, color)` Sets a single pixel directly (convenience wrapper). * `x`: Column (1-30). * `y`: Row (1-10). * `color`: 0xRRGGBB color. ### `overlay_serialize()` Returns the current state of all overlays as a JSON string. --- ## INA (Current/Power Monitor) Functions for reading data from the INA219 current sensors. ### `ina_get_current(sensor)` Reads the current in Amps. * `sensor`: The sensor to read (0 or 1). * **Aliases**: `get_current()` ### `ina_get_voltage(sensor)` Reads the shunt voltage in Volts. * `sensor`: The sensor to read (0 or 1). * **Aliases**: `get_voltage()` ### `ina_get_bus_voltage(sensor)` Reads the bus voltage in Volts. * `sensor`: The sensor to read (0 or 1). * **Aliases**: `get_bus_voltage()` ### `ina_get_power(sensor)` Reads the power in Watts. * `sensor`: The sensor to read (0 or 1). * **Aliases**: `get_power()` **Example:** ```jython current_mA = ina_get_current(0) * 1000 print("Current: " + str(current_mA) + " mA") ``` --- ## OLED Display Functions for controlling the onboard OLED display with advanced text, graphics, and bitmap capabilities. ### Basic Display Functions #### `oled_print(text, [size=-1])` Displays text on the OLED screen. It can print strings, numbers, and custom Jumperless types. * `text`: The content to display. * `size` (optional): Text size (0=small scrolling, 1=normal, 2=large). If -1 or omitted, uses the default size set by `oled_set_text_size()`. Defaults to 2. **Size Modes:** - **Size 0**: Small scrolling text (perfect for terminal-like output with multiple lines) - **Size 1**: Normal centered text - **Size 2**: Large centered text (default) **Example:** ```jython oled_print("Hello!") # Uses default size oled_print("Big Text", 2) # Large text oled_print("Scrolling", 0) # Small scrolling text ``` #### `oled_clear([show=True])` Clears the OLED display. * `show` (optional): If `True` (default), automatically calls `oled_show()` after clearing. Set to `False` for animations to avoid flashing between frames. **Example:** ```jython # Normal use - clears and shows oled_clear() # Animation loop - clear without showing to prevent flashing for frame in range(100): oled_clear(False) # Clear without show # Draw your frame... oled_show() # Show once after drawing ``` #### `oled_show()` Refreshes the OLED display to show the latest changes. **Note:** Usually not needed as `oled_print()` handles display updates automatically. Required after `oled_set_pixel()` calls. #### `oled_connect()` Connects the I2C lines to the OLED display. #### `oled_disconnect()` Disconnects the I2C lines from the OLED display. ### Text Size Control #### `oled_set_text_size(size)` Set the default text size for all subsequent `oled_print()` calls. * `size`: Text size (0=small scrolling, 1=normal, 2=large) * Returns: `True` if successful, `False` if invalid size **Example:** ```jython import jumperless as j # Use small scrolling text for terminal output j.oled_set_text_size(0) for i in range(10): j.oled_print(f"Line {i+1}") # Each creates a new scrolling line # Switch to large text j.oled_set_text_size(2) j.oled_print("BIG TEXT") ``` #### `oled_get_text_size()` Get the current default text size. * Returns: Current text size (0, 1, or 2) **Example:** ```jython current_size = oled_get_text_size() print(f"Current OLED text size: {current_size}") ``` ### Print Redirection #### `oled_copy_print(enable)` Enable or disable copying Python `print()` output to the OLED display in real-time. When enabled, all `print()` statements will appear on both the serial console and the OLED in small scrolling text mode. This is perfect for debugging without a serial connection. * `enable`: `True` to enable, `False` to disable **Example:** ```jython import jumperless as j # Enable print copying j.oled_copy_print(True) # These appear on both serial AND OLED print("Starting test...") voltage = j.adc_get(0) print(f"Voltage: {voltage:.2f}V") print("Test complete!") # Disable print copying j.oled_copy_print(False) print("This only goes to serial") ``` ### Font System #### `oled_get_fonts()` Get a list of all available font families. * Returns: List of font family names (strings) **Available Fonts:** 1. Eurostyle - sans serif, futuristic, like Xenon pinball or Star Trek 2. Jokerman - decorative font with lots of extra artistic dots and bars 3. Comic Sans - sans serif, comic book style lettering 4. Courier New - slab serif, typewriter style 5. New Science - sans serif, humanist/geometric, like Helvetica 6. New Science Ext - wider version of New Science 7. Iosevka Regular - fixed spacing sans serif, like Lucida, slashed zero 8. Berkeley Mono - fixed spacing, sans serif, large X height 9. Pragmatism - sans serif, vaguely romantic 10. Andale Mono - fixed spacing sans serif, used for small text (Descriptions by MadBodger) **Example:** ```jython import jumperless as j import time fonts = j.oled_get_fonts() print(f"Available fonts: {fonts}") # Display each font for font in fonts: j.oled_set_font(font) j.oled_print(font, 2) time.sleep(1.5) ``` #### `oled_set_font(name)` Set the current font family by name. The font will remain active until changed. * `name`: Font family name (case-insensitive) * Returns: `True` if successful, `False` if font not found **Example:** ```jython import jumperless as j # Set to Jokerman if j.oled_set_font("Jokerman"): j.oled_print("Fun Font!", 2) else: print("Font not found") # Set to Courier for code-like display j.oled_set_font("Courier New") j.oled_print("Code Style", 2) ``` #### `oled_get_current_font()` Get the name of the currently active font family. * Returns: Current font family name (string) **Example:** ```jython import jumperless as j current = j.oled_get_current_font() print(f"Current font: {current}") # Save and restore font saved_font = j.oled_get_current_font() j.oled_set_font("Jokerman") j.oled_print("Temporary", 2) j.oled_set_font(saved_font) # Restore original ``` ### Bitmap Display #### `oled_load_bitmap(filepath)` Load a bitmap file into the internal bitmap buffer. * `filepath`: Path to bitmap file (e.g., "/images/logo.bin") * Returns: `True` if loaded successfully, `False` on error **Bitmap File Format:** - **Raw Format**: Pure bitmap data (guesses dimensions from file size) - 128x32 = 512 bytes - 128x64 = 1024 bytes - 64x32 = 256 bytes - **Custom Format**: 4-byte header + bitmap data - Bytes 0-1: Width (16-bit little-endian) - Bytes 2-3: Height (16-bit little-endian) - Remaining: Bitmap data (1 bit per pixel, packed) **Example:** ```jython if oled_load_bitmap("/images/jogo32h.bin"): print("Logo loaded!") else: print("Failed to load logo") ``` #### `oled_display_bitmap(x, y, width, height, [data=None])` Display a bitmap on the OLED. **Two modes:** 1. **Use loaded bitmap**: If `data` is `None`, displays the bitmap from `oled_load_bitmap()` 2. **Direct data**: If `data` is provided, displays that bitmap immediately * `x`: X position on display (0-127) * `y`: Y position on display (0-31) * `width`: Bitmap width in pixels (ignored if using loaded bitmap) * `height`: Bitmap height in pixels (ignored if using loaded bitmap) * `data` (optional): Bitmap data to display directly * Returns: `True` if successful, `False` on error **Example:** ```jython import jumperless as j # Method 1: Load then display j.oled_load_bitmap("/images/logo.bin") j.oled_display_bitmap(0, 0, 0, 0) # width/height ignored for loaded bitmap # Method 2: Display direct data bitmap_data = bytes([0xFF, 0x00, 0xFF, 0x00] * 128) # Striped pattern j.oled_display_bitmap(0, 0, 128, 32, bitmap_data) ``` #### `oled_show_bitmap_file(filepath, x, y)` Convenience function that loads and displays a bitmap in one call. * `filepath`: Path to bitmap file * `x`: X position on display * `y`: Y position on display * Returns: `True` if successful, `False` on error **Example:** ```jython import jumperless as j import time # One-liner to show a logo j.oled_show_bitmap_file("/images/jogo32h.bin", 0, 0) time.sleep(2) # Show another image j.oled_show_bitmap_file("/images/badge.bin", 32, 8) ``` ### Framebuffer and Pixel Manipulation The framebuffer functions provide low-level access to the display memory, enabling advanced graphics, animations, and screen capture. **Framebuffer Format:** - 1 bit per pixel (0=black/off, 1=white/on) - Organized in vertical bytes (8 pixels per byte) - Size: 512 bytes (128x32) or 1024 bytes (128x64) - Compatible with Adafruit SSD1306 format #### `oled_get_framebuffer()` Get a copy of the current OLED framebuffer as a bytes object. * Returns: Framebuffer data as `bytes` (512 or 1024 bytes depending on display size) #### `oled_set_framebuffer(data)` Set the entire OLED framebuffer from bytes or bytearray. * `data`: Framebuffer data (must be correct size for display) * Returns: `True` if successful, `False` if wrong size **Example:** ```jython import jumperless as j import time # Capture current display fb = j.oled_get_framebuffer() print(f"Framebuffer size: {len(fb)} bytes") # Save to file for later with open("/screen_capture.bin", "wb") as f: f.write(fb) j.oled_clear() time.sleep(2) # Load and display saved screen with open("/screen_capture.bin", "rb") as f: fb_data = f.read() if j.oled_set_framebuffer(fb_data): print("Screen restored!") else: print("Wrong framebuffer size") ``` #### `oled_get_framebuffer_size()` Get the dimensions and size of the framebuffer. * Returns: Tuple of `(width, height, buffer_size_in_bytes)` **Example:** ```jython width, height, size = oled_get_framebuffer_size() print(f"Display: {width}x{height}, {size} bytes") # Output: Display: 128x32, 512 bytes # Calculate pixels total_pixels = width * height print(f"Total pixels: {total_pixels}") ``` #### `oled_set_pixel(x, y, color)` Set a single pixel on the OLED. **Note:** Call `oled_show()` after setting pixels to make changes visible. * `x`: X coordinate (0 to width-1) * `y`: Y coordinate (0 to height-1) * `color`: Pixel color (0=black/off, 1=white/on) * Returns: `True` if successful, `False` if OLED not connected **Example:** ```jython import jumperless as j # Draw a diagonal line j.oled_clear() for i in range(32): j.oled_set_pixel(i, i, 1) j.oled_show() # Draw a box for x in range(20, 108): j.oled_set_pixel(x, 10, 1) # Top edge j.oled_set_pixel(x, 22, 1) # Bottom edge for y in range(10, 23): j.oled_set_pixel(20, y, 1) # Left edge j.oled_set_pixel(107, y, 1) # Right edge j.oled_show() ``` #### `oled_get_pixel(x, y)` Get the color value of a single pixel. * `x`: X coordinate (0 to width-1) * `y`: Y coordinate (0 to height-1) * Returns: Pixel color (0=black/off, 1=white/on, -1=error) **Example:** ```jython # Check if a pixel is set pixel = oled_get_pixel(64, 16) if pixel == 1: print("Pixel is white/on") elif pixel == 0: print("Pixel is black/off") else: print("Error reading pixel") ``` ### OLED Layout / Screens (retained, live-updating) The layout system lets you build a **retained screen** out of positioned text and shapes, then `show()` it. A background render service keeps the screen up to date: any text containing `{token}` templates re-resolves automatically, and you can move/restyle elements live. This is ideal for stats pages and for designing layouts you intend to bake into firmware. It is available as an object-oriented wrapper in the `oledgui` module, or as a flat handle-based API on the native `jumperless` module. #### Object-oriented API (`oledgui`) ```python from oledgui import Screen, Text, Line, Rect, set_var, ALIGN_CENTER, ALIGN_TOP scr = Screen() # Title centered along the top, divider beneath it scr.add(Text("JUMPERLESS", font="Eurostile", size=8, halign=ALIGN_CENTER, valign=ALIGN_TOP)) scr.add(Line(0, 11, 127, 11)) # Live readouts - these update on their own every render tick scr.add(Text("A0 {adc:0}V", x=2, y=14, font="Pragmatism", size=8)) scr.add(Text("up {uptime}", x=2, y=23, font="Pragmatism", size=7)) scr.show() # becomes the active display # Push your own values into a {name} token set_var("status", "ready") scr.save("dashboard") # -> /screens/dashboard.json ``` **Live value tokens** (resolved in the background, no redraw loop needed): | Token | Value | |-------|-------| | `{adc:N}` | ADC channel N voltage | | `{gpio:N}` | GPIO N state (`HIGH`/`LOW`) | | `{dac:N}` | DAC channel N voltage | | `{uptime}` | time since boot (`m:ss`) | | `{freemem}` | free heap bytes | | `{undo}` | last undo/redo action label | | `{name}` | any custom value pushed with `set_var("name", value)` | **Fonts** are selected by name + point size (nearest available size is used, falling back to Pragmatism for unknown names). Get available names with `oled_get_fonts()`. **Positioning** is absolute `(x, y)` by default; pass `halign`/`valign` (or call `.anchor(h, v)`) to anchor an element so it adapts to content width. **Classes:** * `Screen()` — `.add(el)`, `.text(...)`, `.shape(...)`, `.show()`, `.hide()`, `.clear()`, `.save(name)`, `.free()` * `Text(text, x, y, font, size, halign, valign, z)` — live props: `.text`, `.font`, `.size`, `.x`, `.y`, `.z`, `.visible` * `Shape(kind, x, y, w, h, filled, z)` plus helpers `Line(x, y, x2, y2)` and `Rect(x, y, w, h, filled=False)` * `load_screen(name)` → `Screen` (loads `/screens/.json`) #### Flat API (native `jumperless`) The OO classes are built on these; use them directly if you prefer: * `oled_screen()` → screen handle * `oled_add_text(screen, text, x=, y=, font=, size=, halign=-1, valign=-1, z=0)` → element handle * `oled_add_shape(screen, kind, x=, y=, w=, h=, filled=, z=)` → element handle (kind: 0=line, 1=rect, 2=filled rect) * `oled_set(elem, prop, value)` — `prop` is `text`/`font` (str) or `x`/`y`/`w`/`h`/`z`/`size`/`visible`/`anchor`/`halign`/`valign`/`shape`/`filled` (int) * `oled_set_var(name, value)` — push a live `{name}` value * `oled_screen_show(screen)` / `oled_screen_hide()` * `oled_screen_save(screen, name)` / `oled_screen_load(name)` → screen handle * `oled_screen_clear(screen)` / `oled_screen_free(screen)` ```python import jumperless as j s = j.oled_screen() j.oled_add_text(s, "Hello {uptime}", x=0, y=0, font="Pragmatism", size=10) j.oled_screen_show(s) ``` #### Designing layouts to bake into firmware Build and tweak a screen live (e.g. with the `oled_layout_editor.py` example, driven by the clickwheel + probe), then `save()` it. The resulting `/screens/.json` is human-readable — read the `font`, `size`, `x`, `y`, `halign`/`valign` values straight out of it and copy them into native C++ code. ### Advanced OLED Examples #### Debug Output to OLED ```jython import jumperless as j import time # Enable print copying for debugging j.oled_copy_print(True) # Your code with debug output for i in range(10): voltage = j.adc_get(0) print(f"V{i}: {voltage:.2f}V") time.sleep(0.5) j.oled_copy_print(False) ``` #### Simple Oscilloscope ```jython import jumperless as j import math import time # Clear display j.oled_clear() width, height, _ = j.oled_get_framebuffer_size() j.connect(j.ADC0, 11) # Draw sine wave animation for offset in range(100): j.oled_clear(False) # Don't show() after clear to avoid flashing # Draw axes for x in range(width): if (x % 4 == 0): j.oled_set_pixel(x, height//2, 1) # Dotted center line # Draw sine wave for x in range(width): # y = int(height//2 + 10 * math.sin((x + offset) / 10)) # Dummy sine wave y = int(j.get_adc(0)) + 16 if 0 `, `>=` * **Arithmetic**: `+`, `-`, `*`, `//`, `%` * **Conversions**: `int(pad)` converts to integer value **Example:** ```jython pad = probe_read(False) # Non-blocking if pad != NO_PAD: print(pad) if pad 60: # Works! node_2 -= 60 connect(node_1, node_2) ``` ### `probe_button([blocking=True], [consume=False])` Reads the state of the buttons on the probe. * `blocking` (optional): If `True` (default), waits for a button press. If `False`, returns the current state immediately. * `consume` (optional): If `True`, clears the button press after reading (one-shot detection). If `False` (default), the button state persists while held, allowing repeated reads in a loop. * Returns a `ProbeButton` object (`CONNECT_BUTTON`, `REMOVE_BUTTON`, or `BUTTON_NONE`). * **Aliases**: `get_button()`, `button_read()`, `read_button()`, `probe_button_blocking()`, `probe_button_nonblocking()`, `check_button()`, `button_check()` **Consume Behavior:** - `consume=False` (default): Holding the button returns the same state repeatedly - **ideal for continuous control** (e.g., incrementing values while held) - `consume=True`: Each button press is detected only once - ideal for menu navigation or one-shot actions **Example:** ```jython # Continuous control - hold button to keep changing hue while True: button = check_button() # consume=False by default if button == BUTTON_CONNECT: hue += 1 # Keeps incrementing while button is held set_net_color_hsv(0, hue) elif button == BUTTON_REMOVE: hue -= 1 # Keeps decrementing while button is held set_net_color_hsv(0, hue) time.sleep(0.05) # One-shot detection - each press counted once presses = 0 while presses high threshold - Switches to MEASURE when current {menu_items[selected]}") elif direction == j.CLICKWHEEL_DOWN: selected = (selected - 1) % len(menu_items) print(f"> {menu_items[selected]}") # Check button button = j.clickwheel_get_button() if button == j.CLICKWHEEL_PRESSED: print(f"Selected: {menu_items[selected]}") elif button == j.CLICKWHEEL_DOUBLECLICKED: print("Exit menu") break # Note: Direction persists, so even with slow polling (50ms), # you won't miss turn events! time.sleep(0.05) ``` **Example - Value Adjustment with Persistence:** ```jython import jumperless as j import time value = 50 while True: # Check direction without consuming direction = j.clickwheel_get_direction(consume=False) if direction == j.CLICKWHEEL_UP: value = min(100, value + 1) j.oled_print(f"Value: {value}", 2) time.sleep(0.1) # Delay for visual feedback # Consume after displaying j.clickwheel_get_direction(consume=True) elif direction == j.CLICKWHEEL_DOWN: value = max(0, value - 1) j.oled_print(f"Value: {value}", 2) time.sleep(0.1) j.clickwheel_get_direction(consume=True) # Exit on button press if j.clickwheel_get_button() == j.CLICKWHEEL_PRESSED: break time.sleep(0.01) ``` --- ## WaveGen (Waveform Generator) Functions for generating analog waveforms on DAC outputs. **Setters:** - `wavegen_set_output(output)` - Select output: `DAC0`, `DAC1`, `TOP_RAIL`, `BOTTOM_RAIL` (default `DAC1`) - `wavegen_set_freq(hz)` - Set frequency: 0.0001–10000.0 Hz (default 100 Hz) - `wavegen_set_wave(shape)` - Set waveform shape (see constants below) - `wavegen_set_sweep(start_hz, end_hz, seconds)` - Configure a linear sweep - `wavegen_set_amplitude(vpp)` - 0.0–16.0 Vpp (default 3.3 Vpp) - `wavegen_set_offset(v)` - -8.0–+8.0 V (default 1.65 V for 0–3.3 V centered) - `wavegen_start([run=True])` - Start/stop output; calling with False stops - `wavegen_stop()` - Stop output immediately **Getters:** - `wavegen_get_output()` - Get current output channel - `wavegen_get_freq()` - Get current frequency - `wavegen_get_wave()` - Get current waveform - `wavegen_get_amplitude()` - Get current amplitude (Vpp) - `wavegen_get_offset()` - Get current offset - `wavegen_is_running()` - Check if wavegen is active **Aliases:** All setters/getters have `set_wavegen_*` and `get_wavegen_*` aliases. **Waveform Constants:** - `SINE` (0) - Sine wave - `TRIANGLE` (1) - Triangle wave - `SAWTOOTH` (2) - Sawtooth/ramp wave - `RAMP` (2) - Alias for SAWTOOTH - `SQUARE` (3) - Square wave - `ARBITRARY` (4) - Arbitrary waveform (not yet implemented) **Example:** ```jython # Generate a 100Hz sine wave on DAC1 wavegen_set_output(DAC1) wavegen_set_wave(SINE) wavegen_set_freq(100) wavegen_set_amplitude(3.3) # 3.3V peak-to-peak wavegen_set_offset(1.65) # Center at 1.65V (0-3.3V range) wavegen_start() # Check if running if wavegen_is_running(): print("Wavegen active at " + str(wavegen_get_freq()) + "Hz") # Change waveform while running wavegen_set_wave(TRIANGLE) # Stop wavegen_stop() ``` **Notes:** - Wavegen runs on core 2 and is fully blocking while active; LEDs and routing updates pause until `wavegen_stop()`. - Frequency, waveform, amplitude, and offset can be changed live while running. --- ## Net Information API Functions for querying and modifying net metadata. Nets are groups of connected nodes. ### `get_net_name(netNum)` Gets the name of a specific net. * `netNum`: The net number (0 to number of nets - 1). * Returns the net name string, or `None` if the net doesn't exist. **Example:** ```jython name = get_net_name(0) print("Net 0 is called: " + str(name)) ``` ### `set_net_name(netNum, name)` Sets a custom name for a net. * `netNum`: The net number. * `name`: The new name string. Pass empty string or `None` to reset to default. **Example:** ```jython set_net_name(0, "VCC") set_net_name(1, "Signal_A") ``` ### `get_net_color(netNum)` Gets the color of a net as a 32-bit RGB value. * `netNum`: The net number. * Returns the color as `0xRRGGBB`. **Example:** ```jython color = get_net_color(0) print("Net color: " + hex(color)) # e.g., "0xff0000" for red ``` ### `get_net_color_name(netNum)` Gets the color name of a net as a human-readable string. * `netNum`: The net number. * Returns a color name like "red", "blue", "green", etc. **Example:** ```jython color_name = get_net_color_name(0) print("Net 0 is " + color_name) # e.g., "Net 0 is red" ``` ### `set_net_color(netNum, color, [r], [g], [b])` Sets the color of a net by name, hex string, or RGB values. * `netNum`: The net number. * `color`: Color as a name ("red", "blue", "pink") or hex string ("#FF0000", "0xFF0000"). * `r`, `g`, `b` (optional): If providing RGB values directly, pass them as separate arguments. * Returns `1` on success, `0` on failure (invalid color). **Available color names:** red, orange, amber, yellow, chartreuse, green, seafoam, cyan, blue, royal blue, indigo, violet, purple, pink, magenta, brown, white, black, grey **Example:** ```jython # Set by color name set_net_color(0, "red") set_net_color(1, "cyan") # Set by hex string set_net_color(2, "#FF00FF") # Magenta set_net_color(3, "0x00FF00") # Green # Set by RGB values set_net_color(4, 255, 128, 0) # Orange ``` ### `set_net_color_hsv(netNum, h, [s], [v])` Sets the color of a net using HSV (Hue, Saturation, Value) color space. Automatically detects whether you're using normalized (0.0-1.0) or full-range (0-255) values based on the hue parameter. * `netNum`: The net number. * `h`: Hue value. If `h` is between 0.0-1.0, all HSV values are treated as normalized (0.0-1.0). Otherwise, values are treated as 0-255 range. * `s` (optional): Saturation value. Defaults to maximum saturation (255) if not provided or negative. * `v` (optional): Value/brightness. Defaults to 32 (reasonable LED brightness) if not provided or negative. * Returns `1` on success, `0` on failure. **Range auto-detection:** - If `h` ≤ 1.0: Normalized mode (0.0-1.0 for all values) - If `h` > 1.0: Full-range mode (0-255 for all values) **Default brightness (32):** This provides good visibility without being overly bright. For maximum brightness, explicitly set `v` to 1.0 (normalized) or 255 (full-range). **Example:** ```jython # Normalized mode (0.0-1.0) - detected because h 1.0 set_net_color_hsv(6, 0) # Pure red, max saturation, default brightness (32) set_net_color_hsv(7, 85) # Green (85 ≈ 255/3), default brightness (32) set_net_color_hsv(8, 170) # Blue (170 ≈ 2*255/3), default brightness (32) set_net_color_hsv(9, 128, 200) # Cyan-ish with 200/255 saturation, default brightness set_net_color_hsv(10, 128, 255, 128)# Cyan with max saturation, 50% brightness set_net_color_hsv(11, 128, 255, 255)# Cyan at MAXIMUM brightness (255) # Using all defaults (h only) set_net_color_hsv(12, 0.25) # Yellow with full saturation, brightness 32 set_net_color_hsv(13, 64) # Yellow in 0-255 mode, brightness 32 ``` **Why use HSV?** HSV is often more intuitive for color selection than RGB: - **Hue** represents the actual color (red → yellow → green → cyan → blue → magenta → red) - **Saturation** controls color intensity (0 = grayscale, max = vivid color) - **Value** controls brightness (0 = black, max = full brightness) ### `get_num_nets()` Gets the number of currently active nets. * Returns an integer. **Example:** ```jython num = get_num_nets() print("There are " + str(num) + " nets") ``` ### `get_num_bridges()` Gets the total number of bridges (connections). * Returns an integer. **Example:** ```jython num = get_num_bridges() print("There are " + str(num) + " bridges") ``` ### `get_net_nodes(netNum)` Gets all nodes in a net as a comma-separated string. * `netNum`: The net number. * Returns a string like "D13,TOP_RAIL,GPIO_1". **Example:** ```jython nodes = get_net_nodes(0) print("Net 0 contains: " + nodes) ``` ### `get_bridge(bridgeIdx)` Gets information about a specific bridge. * `bridgeIdx`: The bridge index (0 to number of bridges - 1). * Returns a tuple `(node1, node2, duplicates)`. **Example:** ```jython bridge = get_bridge(0) print("Bridge 0: " + str(bridge[0]) + " to " + str(bridge[1])) ``` ### `get_net_info(netNum)` Gets comprehensive information about a net as a dictionary. * `netNum`: The net number. * Returns a dict with keys: `name`, `number`, `color`, `color_name`, `nodes`. **Example:** ```jython info = get_net_info(0) print("Net name: " + info['name']) print("Net color: " + info['color_name']) print("Net nodes: " + info['nodes']) ``` --- ## Path Query API Functions for inspecting the internal routing paths between nodes. Paths represent the actual physical routes through the crossbar chips that connect nodes together. ### Understanding Paths vs Bridges * **Bridges**: Direct connections you create (e.g., `connect(1, 5)`) * **Paths**: The actual routing through crossbar chips to implement those bridges A single bridge may require multiple paths through different chips. The path query API lets you inspect these internal routing details. ### `get_num_paths([include_duplicates=True])` Gets the number of routing paths currently in use. * `include_duplicates` (optional): If `True` (default), count all paths including duplicates. If `False`, count only primary (non-duplicate) paths. * Returns an integer. **Example:** ```jython total = get_num_paths() # All paths including duplicates primary = get_num_paths(False) # Only primary paths print(f"Total paths: {total}, Primary: {primary}") ``` ### `get_path_info(path_idx)` Gets detailed information about a specific routing path. * `path_idx`: The path index (0 to `get_num_paths()-1`) * Returns a dict with keys: `node1`, `node2`, `net`, `chips`, `x`, `y`, `duplicate` * Returns `None` if index is invalid **Example:** ```jython path = get_path_info(0) if path: print(f"Path from {path['node1']} to {path['node2']}") print(f"Uses chips: {path['chips']}") print(f"Is duplicate: {path['duplicate']}") ``` ### `get_all_paths()` Gets all routing paths as a list of dictionaries. * Returns a list of path dicts (same format as `get_path_info()`) **Example:** ```jython paths = get_all_paths() for i, path in enumerate(paths): print(f"Path {i}: {path['node1']} -> {path['node2']} via net {path['net']}") ``` ### `get_path_between(node1, node2)` Queries the routing path between two specific nodes. * `node1`, `node2`: The nodes to query * Returns a path dict if found, `None` otherwise **Example:** ```jython path = get_path_between(1, 5) if path: print(f"Route uses chips: {path['chips']}") print(f"Crossbar coordinates: x={path['x']}, y={path['y']}") else: print("No path found between nodes 1 and 5") ``` --- ## Net Current Scan API The Jumperless (V5 only) continuously measures the voltage of every routed node in the background by momentarily tapping a free ADC onto each one, then derives the current through every connection from the voltage difference across its known crosspoint resistance. These are the same numbers that drive the marching-ant current animation on the LEDs, and scripts can query them directly. **How the scan behaves:** * Enabled by the `net_currents` setting in the `[display]` config section (on by default) — the `i` serial command toggles it. When the scan is off, every function below returns `None`. * The scan round-robins through all routed nodes, refreshing the full board about once per second; currents are recomputed from the latest voltages 20 times per second. * It is completely passive: the ADC tap is high impedance (~1 MΩ) and never disturbs your circuit. * **DC only** — nodes carrying PWM or other oscillating signals are rejected by the scan's drift check and read `None`. * Accuracy is roughly ±10%, tied to the `crosspoint_resistance` value in the `[calibration]` config section. * Reported currents are always **conventional** (positive = higher potential toward lower). The `current_flow` display setting only flips the LED animation direction, never these numbers. ### `get_node_voltage(node)` Gets the scanned voltage of any routed node. * `node`: Node number, name, or node object (e.g. `15`, `"D2"`, `TOP_RAIL`) * Returns the voltage in volts, or `None` when the scan has no fresh sample (scan disabled, node not routed, or the node is floating) * Alias: `node_voltage()` **Example:** ```jython v = get_node_voltage(15) if v is not None: print(f"Row 15 sits at {v:.2f} V") else: print("Row 15 is not routed (or floating)") ``` ### `get_net_current(netNum)` Gets the current flowing in a net's dominant path. * `netNum`: Net number (1 to `get_num_nets()`) * Returns a dict with keys: `current_mA` (magnitude), `voltage` (midpoint voltage, used for the ant color), `from_node`, `to_node` (conventional current flows from → to) * Returns `None` when the scan has no data for this net * Alias: `net_current()` **Example:** ```jython info = get_net_current(4) if info: print(f"{info['current_mA']:.2f} mA flowing from {info['from_node']} to {info['to_node']}") ``` ### `get_path_current(path_idx)` Gets the signed current through one routing path. Uses the same index space as `get_path_info()`, so you can match currents to specific connections. * `path_idx`: Path index (0 to `get_num_paths()-1`) * Returns the current in mA; **positive means conventional current flows `node1` → `node2`** (matching `get_path_info()`'s fields) * Returns `None` when the scan has no data for this path (unrouted, floating, or the scan is off) * Alias: `path_current()` **Example:** ```jython # Print the current through every connection on the board for i in range(get_num_paths(False)): path = get_path_info(i) i_mA = get_path_current(i) if path and i_mA is not None: src, dst = (path['node1'], path['node2']) if i_mA >= 0 else (path['node2'], path['node1']) print(f"{src} -> {dst}: {abs(i_mA):.2f} mA") ``` --- ## Fake GPIO (Virtual GPIO Pins) Fake GPIO uses the Jumperless' internal crossbar switches to create virtual GPIO pins on any routable node. Unlike the RP2350's hardware GPIO (limited to 10 routable pins at 0-3.3V), Fake GPIO lets you use any node with custom voltage levels from -8V to +8V, or as inputs, you can have as many as you like and the ADC will switch to that node and take a reading on demand. *This is still pretty new and will probably change in the future* *Note: Fake GPIO Outputs are currently disabled until I fix the code, they were causing dead shorts between supplies* ### Why Use Fake GPIO? The RP2350's hardware GPIO is limited to 10 routable pins (GPIO_1 through GPIO_8, plus UART TX/RX) with fixed 3.3V logic levels. Fake GPIO extends this by using the crossbar switching matrix to create virtual GPIO pins on any routable node with configurable voltage levels. ### Configuring INPUT Pins ```python FakeGpioPin(node, j.INPUT, threshold_high, threshold_low) ``` Creates a digital input that reads voltage via the ADC. **Parameters:** - `node` (int, required): Any routable node to read from - `mode` (constant, required): `j.INPUT` - `threshold_high` (float, optional): Input HIGH threshold in volts (default: 2.0) - `threshold_low` (float, optional): Input LOW threshold in volts (default: 0.8) **Returns:** FakeGpioPin object **Example:** ```jython import jumperless as j # Read a digital signal button = j.FakeGpioPin(20, j.INPUT, 0.8, 2.0) state = button.value() # Returns 0 or 1 # Custom thresholds for 5V logic signal_5v = j.FakeGpioPin(25, j.INPUT, 3.5, 1.5) if signal_5v.value(): print("5V logic HIGH detected") ``` ### FakeGpioPin Methods #### `pin.value([val])` For INPUT: Reads the current pin state (0 or 1). ```jython # Basic usage pin = j.FakeGpioPin(20, j.INPUT, 0.8, 2.0) # Using value() state = pin.value() # Read current state ``` --- ## System Functions ### `arduino_reset()` Resets the connected Arduino Nano. ### `run_app(appName)` Launches a built-in Jumperless application. * `appName`: The name of the app to run (e.g., "File Manager", "I2C Scan"). ### `pause_core2(pause)` Pauses or resumes core2 processing. * `pause`: `True` to pause core2, `False` to resume. *For doing time sensitive things where you don't want core 2 to cause timing glitches while it periodically updates the LEDs.* Core 2 will resume when you exit the REPL no matter what. ### `send_raw(chip, x, y, setOrClear)` Sends raw data to core2 for direct chip control. * `chip`: Chip identifier (string, e.g., "A", "B", "C"). * `x`, `y`: Coordinates for the operation. * `setOrClear`: `1` to set, `0` to clear. You should probably be looking at the schematic if you use this. By bypassing all the routing logic, you can make or break connections in ~1 µs. Pretty handy if want to do high speed switching between known states. ### `change_terminal_color(color, [flush=True])` Sets the terminal text color using 256-color ANSI codes. * `color`: Color index (0-255), or -1 to reset to default * `flush` (optional): Flush output immediately (default: `True`) This is the 6x6x6 color cube that terminals use: [Image: TermColors] **Example:** ```jython change_terminal_color(196) # Bright red print("Error message") change_terminal_color(-1) # Reset to default change_terminal_color(46) # Cyan print("Info message") ``` ### `cycle_term_color([reset=False], [step=1.0], [flush=True])` Cycles through the terminal color palette. * `reset` (optional): If `True`, reset to start of color sequence * `step` (optional): Color increment step (defaults to the last one you set) * `flush` (optional): Flush output immediately (default: `True`) Useful for creating rainbow effects or visually distinguishing output sections. This is really just a helper function for me, it only uses a subset of the colors (I think ~44 of them?) above that actually look good. It takes a float so you can use fractional values if you have more than 44 things and you want a smooth spectrum. *This doesn't currently work in Viper IDE because it strips the escape sequences* **Example:** ```jython cycle_term_color(True, 1.5) # Start fresh for i in range(10): cycle_term_color() # Next color print(f"Line {i} in different color") # Visual separator in help output cycle_term_color(True, step=5.0) ``` [Image: CycleTerm] ### `force_service(name)` Forces immediate execution of a specific system service by name. * `name`: Service name as a string (e.g., `"ProbeButton"`, `"Peripherals"`). * Returns: `True` if service was found and executed, `False` otherwise. Useful for manually triggering specific services during tight loops where automatic service scheduling might not run frequently enough. **Example:** ```jython while True: # Fast loop that might miss automatic service updates connect(1, 2) force_service("ProbeButton") # Ensure button state updates button = check_button() time.sleep(0.001) ``` ### `force_service_by_index(index)` Forces immediate execution of a specific system service by index (faster than name lookup). * `index`: Service index (integer, obtained via `get_service_index()`). * Returns: `True` if index valid and service executed, `False` otherwise. **Example:** ```jython # Cache the index once for maximum speed btn_idx = get_service_index("ProbeButton") while True: connect(1, 2) force_service_by_index(btn_idx) # Fastest way to force service button = check_button() time.sleep(0.001) ``` ### `get_service_index(name)` Gets the index of a service by name for use with `force_service_by_index()`. * `name`: Service name as a string. * Returns: Service index (integer, 0 or higher), or `-1` if not found. Cache the returned index for repeated fast calls to `force_service_by_index()`. **Example:** ```jython # Look up once, use many times probe_idx = get_service_index("ProbeButton") if probe_idx >= 0: # Use the cached index in your loop force_service_by_index(probe_idx) ``` ### `switch_slot(slot)` Switches to a different connection slot. * `slot`: The slot number to switch to (0-7). * Returns the previous slot number. **Example:** ```jython old_slot = switch_slot(2) # Switch to slot 2 print("Was in slot: " + str(old_slot)) ``` ### `context_toggle()` Toggles the connection context between `global` and `python` modes. * In **global** mode: Connection changes persist after exiting Python. * In **python** mode: Connection state is restored when you exit the REPL. **Example:** ```jython context_toggle() # Switch from global to python (or vice versa) print("Now in " + context_get() + " mode") ``` ### `context_get()` Gets the current connection context name. * Returns `"global"` or `"python"`. **Example:** ```jython if context_get() == "global": print("Changes will persist after exit") else: print("Changes will be discarded on exit") ``` ### `get_state()` Returns the entire board state as a formatted JSON string. This includes nets, power settings, and GPIO configuration. **Example:** ```jython state_json = get_state() print(state_json) ``` ### `set_state(json, [clear_first=True], [from_wokwi=False])` Applies a board state from a JSON string or, if ``from_wokwi`` is True, from a Wokwi diagram (provided as raw JSON or filepath on the board). * `json`: A JSON string representing the state (same format as returned by `get_state()`), or a filename/Wokwi JSON when ``from_wokwi`` is used. * `clear_first` (optional): If `True` (default), clears all existing connections and FakeGPIO state before applying the new state. Set to `False` to merge the new state with the existing one. * `from_wokwi` (optional): If `True`, interpret `json` as a Wokwi diagram.json and convert it to a Jumperless state before applying. **Examples:** ```jython # Apply a simple state from a string set_state('{"nets": [{"index": 1, "name": "GND", "nodes": ["GND"]}], "power": {"top_rail": 5.0}}') # Apply directly from a Wokwi diagram file saved on the board set_state('/python_scripts/diagram1.json', from_wokwi=True) # or supply raw Wokwi JSON text set_state(wokwi_json_string, from_wokwi=True) ``` --- ## Status Functions These functions print detailed status information to the serial console. * `print_bridges()`: Prints all active bridges. * `print_paths()`: Prints all resolved paths between nodes. * `print_crossbars()`: Prints the raw state of the crossbar matrix. * `print_nets()`: Prints the current net list. * `print_chip_status()`: Prints the status of the CH446Q chips. --- ## JFS (Jumperless FileSystem) The `jfs` module is basically like MicroPython's `vfs` and parts of `os`, but kinda written in a style that's probably more familiar to Arduino-style C++ people (me). It uses the *almost* standardized API shared by [`FatFS` (the one Jumperless actually uses), `LittleFS`, and `SDFS`](https://arduino-pico.readthedocs.io/en/latest/fs.html#file-system-object-littlefs-sd-sdfs-fatfs), but still has been *Pythonified* to use types that are easier to work with in MicroPython. ### Usage ```jython import jfs #you don't actually need this, jfs is imported globally by default # List files in the root directory files = jfs.listdir('/') # returns a python list print(files) ``` Output: ```jython ['config.txt', 'slots/', 'python_scripts/'] ``` (Directories come back with a trailing `/`. Older filesystems may also show legacy `nodeFileSlot*.txt` files and a `net_colors/` directory at the root.) If you want to make this print with subdirectories ```jython files = jfs.listdir('/') for file in range(len(files)): print(files[file]) if files[file].endswith("/"): subdir = jfs.listdir(files[file]) for i in range(len(subdir)): print(" - " + subdir[i]) ``` Output: ``` config.txt slots/ - slot0.yaml - slot1.yaml - slot2.yaml python_scripts/ - history.txt - examples/ - lib/ - script_1.py - script_2.py - log.txt ``` --- ### File API File objects returned by `jfs.open()` support method calls directly on the object: ```jython # Object-oriented file operations # Write-only mode f = jfs.open('hello.txt', 'w') f.write('Hello, Jumperless!') f.close() # Read from the file (need to reopen or use w+/r+ mode) f = jfs.open('hello.txt', 'r') content = f.read() # Read from file object size = f.size() # Get file size f.close() # Read-write mode (truncates file) f = jfs.open('hello.txt', 'w+') f.write('Hello, Jumperless!') f.seek(0) # Seek to beginning to read what we wrote content = f.read() # Now this works! f.close() # Context manager support (automatically closes file) with jfs.open('data.txt', 'w+') as f: f.write('This file will be automatically closed') f.seek(0) # Reset to beginning content = f.read() # Read back what we wrote pos = f.tell() # Get current position name = f.name() # Get file name ``` #### Using `f.print()` for logging The `f.print()` method works like Python's `print()` but writes to the file. It automatically converts arguments to strings, joins multiple arguments with spaces, and adds a newline - perfect for logging. **Note:** `f.print()` does *not* flush after each call (a FatFS flush takes ~2 seconds, so auto-flushing would make logging loops crawl). Data is committed when you `f.close()` or `f.seek()` - if you're logging data you can't afford to lose to a crash or reset, call `f.flush()` yourself at checkpoints: ```jython # Great for logging - auto-converts types and adds newlines with jfs.open('log.txt', 'w+') as f: f.print("Starting test...") voltage = adc_get(0) f.print("Voltage:", voltage, "V") # Multiple args work f.print("Test complete!") f.flush() # Commit to flash at a checkpoint # Unlike f.write(), f.print() handles non-strings automatically f = jfs.open('data.txt', 'w') f.print(123) # OK - converts int to string f.print(3.14) # OK - converts float to string f.print("mixed", 42) # OK - multiple args joined with spaces f.close() ``` **Note:** `f.print()` is a file object method only. There is no `jfs.print()` module-level function. Other ways to do the same thing #### 2. Module-Level Functions You can also use module-level functions with file handles: ```jython f = jfs.open('hello.txt', 'w') jfs.write(f, 'Hello, Jumperless!') # Module-level function jfs.seek(f, 0) # Module-level function content = jfs.read(f) # Module-level function jfs.close(f) # Module-level function ``` #### 3. Direct String Operations (For simple cases) The whole-file path-based helpers live in the `jumperless` module (available globally), not in `jfs`: ```jython # Write/read entire files at once (no file handles needed) fs_write('notes.txt', 'key=value\nother=setting') content = fs_read('notes.txt') ``` ### File Modes When using `jfs.open(path, mode)`, the following modes are supported: | Mode | Description | Read | Write | Create | |------|-------------|------|-------|---------| | `'r'` | Read only | ✅ | ❌ | ❌ | | `'w'` | Write only | ❌ | ✅ | ✅ | | `'a'` | Append only | ❌ | ✅ | ✅ | | `'r+'` | Read + Write | ✅ | ✅ | ❌ | | `'w+'` | Read + Write | ✅ | ✅ | ✅ | | `'a+'` | Read + Append | ✅ | ✅ | ✅ | **Important:** You cannot read from a file opened in write-only mode (`'w'` or `'a'`) - reads just come back empty. Use `'w+'`, `'r+'`, or `'a+'` if you need both read and write access. You can also add `'b'` to any mode (e.g. `'rb'`, `'wb'`) to open in binary mode: reads return `bytes` and writes accept `bytes`/`bytearray`/`memoryview`. ### Open a file for writing ```jython f = jfs.open('hello.txt', 'w') f.write('Hello, Jumperless!') # Now works with object-oriented API! f.close() ``` ### Directory Operations #### `jfs.listdir(path)` Returns a list containing the names of the entries in the directory given by `path`. * `path` (str): The path to the directory. **Example:** ```jython # List contents of the root directory print(jfs.listdir('/')) # List contents of a subdirectory jfs.mkdir('/my_dir') print(jfs.listdir('/my_dir')) ``` #### `jfs.mkdir(path)` Create a new directory. * `path` (str): The path of the new directory. #### `jfs.rmdir(path)` Remove an empty directory. * `path` (str): The path of the directory to remove. #### `jfs.remove(path)` Remove a file. * `path` (str): The path of the file to remove. #### `jfs.rename(old_path, new_path)` Rename a file or directory. * `old_path` (str): The current path. * `new_path` (str): The new path. #### `jfs.exists(path)` Check if a file or directory exists. * `path` (str): The path to check. * Returns `True` if it exists, `False` otherwise. #### `jfs.stat(path)` Get status of a file or directory. * `path` (str): The path of the file or directory. * Returns a 10-element tuple in the same order as `os.stat()`. Only the size field (index 6) is really populated - mode and timestamps are placeholders. ### Filesystem Information #### `jfs.info()` Get information about the filesystem. * Returns a tuple `(total_bytes, used_bytes, free_bytes)`. **Example:** ```jython total, used, free = jfs.info() print("Filesystem Size: " + str(total / 1024) + " KB") print("Used: " + str(used / 1024) + " KB") print("Free: " + str(free / 1024) + " KB") ``` ### File I/O The `jfs` module supports standard file opening and handling using `jfs.open()` and file objects, including support for the `with` statement for automatic resource management. #### `jfs.open(path, mode='r')` Open a file and return a corresponding file object. * `path` (str): The path to the file. * `mode` (str, optional): The mode in which the file is opened. Defaults to `'r'`. * `'r'`: Read (default). * `'w'`: Write (creates a new file or truncates an existing one). * `'a'`: Append. * `'r+'`: Read and write. * `'w+'`: Write and read (creates/truncates). * `'a+'`: Append and read. **Example:** ```jython # Open a file for reading f = jfs.open('config.txt', 'r') content = f.read() f.close() # Use 'with' for automatic closing with jfs.open('data.log', 'a') as log_file: log_file.write('New log entry.\\n') ``` ### File Object Methods The file object returned by `jfs.open()` has the following methods: #### `file.read([size])` Read `size` bytes from the file. If `size` is omitted, reads from the current position to the end of the file. Reads are capped at 8192 bytes per call, so loop if you're reading a file bigger than 8 KB. #### `file.write(data)` Write the given string or bytes `data` to the file. Returns the number of bytes written. #### `file.close()` Close the file. A closed file cannot be read or written to. #### `file.seek(offset, [whence])` Change the stream position. * `offset`: The byte offset. * `whence` (optional): * `0`: Seek from the start of the stream (default). Use `jfs.SEEK_SET`. * `1`: Seek from the current position. Use `jfs.SEEK_CUR`. * `2`: Seek from the end of the stream. Use `jfs.SEEK_END`. #### `file.tell()` Return the current stream position. * **Aliases**: `file.position()` #### `file.size()` Return the total size of the file in bytes. #### `file.available()` Return the number of bytes available to be read from the current position to the end of the file. #### `file.name()` Returns the name of the file. (It's a method call, not an attribute - `f.name` gives you the bound method, not the string.) --- ### Module-Level File Operations For convenience, the `jfs` module also provides functions that operate directly on file handles returned by `jfs.open()`. This can be useful in some scripting scenarios but using file object methods is generally preferred for clarity. * `jfs.read(file_handle, [size])` * `jfs.write(file_handle, data)` * `jfs.close(file_handle)` * `jfs.seek(file_handle, offset, [whence])` * `jfs.tell(file_handle)` * `jfs.size(file_handle)` * `jfs.available(file_handle)` **Example:** ```jython file_handle = jfs.open('temp.txt', 'w') jfs.write(file_handle, 'some data') jfs.close(file_handle) ``` --- ## Help Functions ### `help()` Displays a comprehensive list of all available functions and constants in the `jumperless` module. You can also pass it sections, so `help("GPIO")` will just print that section. ### `nodes_help()` Displays a detailed reference for all available node names and their aliases. ## Node Names and Constants The Jumperless module provides extensive node name support with multiple aliases for each node: ```jython # Power rails (multiple aliases supported) TOP_RAIL = 101 # Also: TOPRAIL, T_R, TOP_R BOTTOM_RAIL = 102 # Also: BOT_RAIL, BOTTOMRAIL, BOTRAIL, B_R, BOT_R SUPPLY_3V3 = 103 # Also: 3V3, 3.3V SUPPLY_5V = 105 # Also: 5V, +5V SUPPLY_8V_P = 120 # Also: 8V_P, 8V_POS SUPPLY_8V_N = 121 # Also: 8V_N, 8V_NEG # Ground connections GND = 100 # Also: GROUND TOP_RAIL_GND = 104 # Also: TOP_GND (not actually routable but included for PADs) BOTTOM_RAIL_GND = 126 # Also: BOT_GND, BOTTOM_GND (not actually routable but included for PADs) # DAC outputs DAC0 = 106 # Also: DAC_0, DAC0_5V DAC1 = 107 # Also: DAC_1, DAC1_8V # ADC inputs ADC0 = 110 # Also: ADC_0, ADC0_8V ADC1 = 111 # Also: ADC_1, ADC1_8V ADC2 = 112 # Also: ADC_2, ADC2_8V ADC3 = 113 # Also: ADC_3, ADC3_8V ADC4 = 114 # Also: ADC_4, ADC4_5V ADC7 = 115 # Also: ADC_7, ADC7_PROBE, PROBE # Current sensing ISENSE_PLUS = 108 # Also: ISENSE_POS, ISENSE_P, INA_P, I_P, CURRENT_SENSE_PLUS, ISENSE_POSITIVE, I_POS ISENSE_MINUS = 109 # Also: ISENSE_NEG, ISENSE_N, INA_N, I_N, CURRENT_SENSE_MINUS, ISENSE_NEGATIVE, I_NEG # GPIO pins (multiple naming conventions) GPIO_1 = 131 # Also: RP_GPIO_1, GPIO1, GP_1, GP1 GPIO_2 = 132 # Also: RP_GPIO_2, GPIO2, GP_2, GP2 GPIO_3 = 133 # Also: RP_GPIO_3, GPIO3, GP_3, GP3 GPIO_4 = 134 # Also: RP_GPIO_4, GPIO4, GP_4, GP4 GPIO_5 = 135 # Also: RP_GPIO_5, GPIO5, GP_5, GP5 GPIO_6 = 136 # Also: RP_GPIO_6, GPIO6, GP_6, GP6 GPIO_7 = 137 # Also: RP_GPIO_7, GPIO7, GP_7, GP7 GPIO_8 = 138 # Also: RP_GPIO_8, GPIO8, GP_8, GP8 # UART pins UART_TX = 116 # Also: RP_UART_TX, TX, RP_GPIO_16 UART_RX = 117 # Also: RP_UART_RX, RX, RP_GPIO_17 # Additional RP GPIOs RP_GPIO_18 = 118 # Also: GP_18 RP_GPIO_19 = 119 # Also: GP_19 # Buffer connections BUFFER_IN = 139 # Also: ROUTABLE_BUFFER_IN, BUF_IN, BUFF_IN, BUFFIN BUFFER_OUT = 140 # Also: ROUTABLE_BUFFER_OUT, BUF_OUT, BUFF_OUT, BUFFOUT # Arduino Nano pins (extensive support) D13 = 83 # Also: NANO_D13 D12 = 82 # Also: NANO_D12 D11 = 81 # Also: NANO_D11 D10 = 80 # Also: NANO_D10 D9 = 79 # Also: NANO_D9 D8 = 78 # Also: NANO_D8 D7 = 77 # Also: NANO_D7 D6 = 76 # Also: NANO_D6 D5 = 75 # Also: NANO_D5 D4 = 74 # Also: NANO_D4 D3 = 73 # Also: NANO_D3 D2 = 72 # Also: NANO_D2 D1 = 71 # Also: NANO_D1 D0 = 70 # Also: NANO_D0 # Arduino Nano analog pins A0 = 86 # Also: NANO_A0 A1 = 87 # Also: NANO_A1 A2 = 88 # Also: NANO_A2 A3 = 89 # Also: NANO_A3 A4 = 90 # Also: NANO_A4 A5 = 91 # Also: NANO_A5 A6 = 92 # Also: NANO_A6 A7 = 93 # Also: NANO_A7 # Arduino Nano non-routable hardwired connections VIN = 69 # Unconnected to anything RST0 = 94 # Hardwired to GPIO 18 on the RP2350 RST1 = 95 # Hardwired to GPIO 19 on the RP2350 N_GND0 = 97 # GND N_GND1 = 96 # GND NANO_5V = 99 # Hardwired to USB 5V bus (can also be used to power the Jumperless) NANO_3V3 = 98 # Unconnected (without bridging the solder jumper on the back) # Probe switch position constants SWITCH_MEASURE = 0 # Probe in measure mode SWITCH_SELECT = 1 # Probe in select mode SWITCH_UNKNOWN = -1 # Position unknown # Clickwheel direction constants CLICKWHEEL_NONE = 0 # No movement CLICKWHEEL_UP = 1 # Turned clockwise CLICKWHEEL_DOWN = 2 # Turned counter-clockwise # Clickwheel button state constants CLICKWHEEL_IDLE = 0 # Not pressed CLICKWHEEL_PRESSED = 1 # Just pressed CLICKWHEEL_HELD = 2 # Being held down CLICKWHEEL_RELEASED = 3 # Just released CLICKWHEEL_DOUBLECLICKED = 4 # Double-clicked ``` ## The entire output of help() ```jython >>> help() Jumperless Native MicroPython Module Available help sections: help() or help("all") - Show all functions help("DAC") - DAC functions help("ADC") - ADC functions help("GPIO") - GPIO functions help("PWM") - PWM functions help("WAVEGEN") - Waveform generator help("INA") - INA current/power monitor help("NODES") - Node connections help("NETS") - Net info (names, colors) help("SLOTS") - Slot management help("OLED") - OLED display help("PROBE") - Probe and button functions help("CLICKWHEEL") - Clickwheel (rotary encoder) functions help("STATUS") - Status and debug functions help("FILESYSTEM") - Filesystem functions help("MISC") - Miscellaneous functions help("EXAMPLES") - Usage examples DAC (Digital-to-Analog Converter): dac_set(channel, voltage) - Set DAC output voltage dac_get(channel) - Get DAC output voltage set_dac(channel, voltage) - Alias for dac_set get_dac(channel) - Alias for dac_get channel: 0-3, DAC0, DAC1, TOP_RAIL, BOTTOM_RAIL channel 0/DAC0: DAC 0 channel 1/DAC1: DAC 1 channel 2/TOP_RAIL: top rail channel 3/BOTTOM_RAIL: bottom rail voltage: -8.0 to 8.0V ADC (Analog-to-Digital Converter): adc_get(channel) - Read ADC input voltage get_adc(channel) - Alias for adc_get channel: 0-4 GPIO: gpio_set(pin, value) - Set GPIO pin state gpio_get(pin) - Read GPIO pin state gpio_set_dir(pin, direction) - Set GPIO pin direction gpio_get_dir(pin) - Get GPIO pin direction gpio_set_pull(pin, pull) - Set GPIO pull-up/down gpio_get_pull(pin) - Get GPIO pull-up/down Aliases: set_gpio, get_gpio, set_gpio_dir, get_gpio_dir, etc. pin 1-8: GPIO 1-8 pin 9: UART Tx pin 10: UART Rx value: True/False for HIGH/LOW direction: True/False for OUTPUT/INPUT pull: -1/0/1/2 for PULLDOWN/NO_PULL/PULLUP/BUS_KEEPER PWM (Pulse Width Modulation): pwm(pin, [frequency], [duty]) - Setup PWM on GPIO pin pwm_set_duty_cycle(pin, duty) - Set PWM duty cycle pwm_set_frequency(pin, freq) - Set PWM frequency pwm_stop(pin) - Stop PWM on pin Aliases: set_pwm, set_pwm_duty_cycle, set_pwm_frequency, stop_pwm pin: 1-8 GPIO pins only frequency: 0.001Hz-62.5MHz default 1000Hz duty_cycle: 0.0-1.0 default 0.5 (50%) WaveGen (Waveform Generator): wavegen_set_output(channel) - Set output: DAC0, DAC1, TOP_RAIL, BOTTOM_RAIL wavegen_set_freq(hz) - Set frequency (0.0001-10000 Hz) wavegen_set_wave(shape) - Set waveform shape wavegen_set_amplitude(vpp) - Set amplitude (0-16 Vpp) wavegen_set_offset(v) - Set DC offset (-8 to +8 V) wavegen_start() - Start waveform generation wavegen_stop() - Stop waveform generation Getters: wavegen_get_output(), wavegen_get_freq(), wavegen_get_wave(), wavegen_get_amplitude(), wavegen_get_offset(), wavegen_is_running() Waveform constants: SINE, TRIANGLE, SAWTOOTH (RAMP), SQUARE INA (Current/Power Monitor): ina_get_current(sensor) - Read current in amps ina_get_voltage(sensor) - Read shunt voltage ina_get_bus_voltage(sensor) - Read bus voltage ina_get_power(sensor) - Read power in watts Aliases: get_current, get_voltage, get_bus_voltage, get_power sensor: 0 or 1 Node Connections: connect(node1, node2) - Connect two nodes disconnect(node1, node2) - Disconnect nodes is_connected(node1, node2) - Check if nodes are connected nodes_clear() - Clear all connections set node2 to -1 to disconnect everything connected to node1 Net Information: get_net_name(netNum) - Get net name set_net_name(netNum, name) - Set custom net name get_net_color(netNum) - Get net color as 0xRRGGBB get_net_color_name(netNum) - Get net color name set_net_color(netNum, color) - Set net color by name or hex set_net_color_hsv(netNum, h, [s], [v]) - Set by HSV (auto-detects range) get_num_nets() - Get number of active nets get_num_bridges() - Get number of bridges get_net_nodes(netNum) - Get comma-separated node list get_bridge(bridgeIdx) - Get bridge info tuple get_net_info(netNum) - Get full net info as dict get_num_paths(include_duplicates=True) - Get number of paths (optionally exclude duplicates) Colors: red, orange, yellow, green, cyan, blue, purple, pink, etc. HSV: h=0.0-1.0 or 0-255 (auto), s=0-1/0-255 (default max), v=0-1/0-255 (default 32) Slot Management: nodes_save([slot]) - Save connections to slot nodes_discard() - Discard unsaved changes nodes_has_changes() - Check for unsaved changes switch_slot(slot) - Switch to different slot (0-7) CURRENT_SLOT - Get current slot number Context (controls persistence): context_toggle() - Toggle global/python mode context_get() - Get current mode name OLED Display: oled_print("text") - Display text oled_clear() - Clear display oled_connect() - Connect OLED oled_disconnect() - Disconnect OLED Probe Functions: probe_read([blocking=True]) - Read probe (default: blocking) read_probe([blocking=True]) - Read probe (default: blocking) probe_read_blocking() - Wait for probe touch (explicit) probe_read_nonblocking() - Check probe immediately (explicit) get_button([blocking], [consume]) - Get button (blocking=True, consume=False) probe_button([blocking], [consume]) - Get button (blocking=True, consume=False) check_button([consume]) - Check button non-blocking (consume=False) probe_button_blocking([consume]) - Wait for button (consume=False) probe_button_nonblocking([consume]) - Check button immediate (consume=False) consume=False (default): Holding button returns same state (continuous control) consume=True: Each press detected once (one-shot detection) Probe Switch Functions: get_switch_position() - Get current switch position set_switch_position(pos) - Set switch position manually check_switch_position() - Check switch via current sensing Touch returns: ProbePad object (1-60, D13_PAD, TOP_RAIL_PAD, LOGO_PAD_TOP, etc.) Button returns: CONNECT, REMOVE, or NONE (front=connect, rear=remove) Switch returns: SWITCH_MEASURE (0), SWITCH_SELECT (1), SWITCH_UNKNOWN (-1) Clickwheel (Rotary Encoder): clickwheel_get_position() - Get raw position counter clickwheel_reset_position() - Reset position to 0 clickwheel_get_direction([consume=True]) - Get direction event clickwheel_get_button() - Get button state clickwheel_is_initialized() - Check if clickwheel is ready consume=True (default): Direction cleared after reading (one-shot detection) consume=False: Direction persists until consumed (can read multiple times) Direction returns: CLICKWHEEL_NONE (0), CLICKWHEEL_UP (1), CLICKWHEEL_DOWN (2) Button returns: CLICKWHEEL_IDLE (0), CLICKWHEEL_PRESSED (1), CLICKWHEEL_HELD (2), CLICKWHEEL_RELEASED (3), CLICKWHEEL_DOUBLECLICKED (4) Status: print_bridges() - Print all bridges print_paths() - Print path between nodes print_crossbars() - Print crossbar array print_nets() - Print nets print_chip_status() - Print chip status Filesystem: jfs.open(path, mode) - Open file jfs.read(file, size) - Read from file jfs.write(file, data) - Write to file jfs.close(file) - Close file jfs.seek(file, pos, whence) - Seek in file jfs.tell(file) - Get current position jfs.size(file) - Get file size jfs.available(file) - Get bytes available jfs.exists(path) - Check if file exists jfs.listdir(path) - List directory jfs.mkdir(path) - Create directory jfs.rmdir(path) - Remove directory jfs.remove(path) - Remove file jfs.rename(from, to) - Rename file jfs.stat(path) - Get file status jfs.info() - Get filesystem info fs_read(path) / fs_write(path, s) - Whole-file helpers (jumperless module) Misc: arduino_reset() - Reset Arduino run_app(appName) - Run built-in app pause_core2(pause) - Pause/unpause Core2 (True/False) send_raw(chip, x, y, set) - Send raw data to crossbar chip force_service(name) - Force run a specific service (e.g., "ProbeButton") force_service_by_index(idx) - Force run service by index (faster) get_service_index(name) - Get service index by name (cache for fast calls) Examples (all functions available globally): dac_set(DAC0, 5.0) # Set DAC0 using node constant voltage = get_adc(1) # Read ADC1 using alias connect(TOP_RAIL, D13) # Connect using constants connect(4, 20) # Connect using numbers top_rail = node("TOP_RAIL") # Create node object oled_print("Hello!") # Display text on OLED current = get_current(0) # Read current using alias set_gpio(1, True) # Set GPIO pin high pwm(1, 1000, 0.5) # 1kHz PWM, 50% duty wavegen_set_wave(SINE); wavegen_start() # Start sine wave set_net_color(0, "red") # Color net 0 red set_net_color_hsv(1, 0.5) # Cyan net 1 (HSV hue) nodes_save() # Save current connections pad = probe_read() # Wait for probe touch button = get_button() # Wait for button press ``` ## The entire output of nodes_help() ```jython >>> nodes_help() Jumperless Node Reference ======================== NODE TYPES: Numbered: 1-60 (breadboard) Arduino: D0-D13, A0-A7 (nano header) GPIO: GPIO_1-GPIO_8 (routable GPIO) Power: TOP_RAIL, BOTTOM_RAIL, GND DAC: DAC0, DAC1 (analog outputs) ADC: ADC0-ADC4, PROBE (analog inputs) Current: ISENSE_PLUS, ISENSE_MINUS UART: UART_TX, UART_RX Buffer: BUFFER_IN, BUFFER_OUT THREE WAYS TO USE NODES: 1. NUMBERS (direct breadboard holes): connect(1, 30) # Connect holes 1 and 30 connect(15, 42) # Any number 1-60 2. STRINGS (case-insensitive names): connect("D13", "TOP_RAIL") # Arduino pin to power rail connect("gpio_1", "adc0") # GPIO to ADC (case-insensitive) connect("15", "dac1") # Mix numbers and names 3. CONSTANTS (pre-defined objects): connect(TOP_RAIL, D13) # Using imported constants connect(GPIO_1, A0) # No quotes needed connect(DAC0, 25) # Mix constants and numbers MIXED USAGE: my_pin = "D13" # Create node object from string connect(my_pin, TOP_RAIL) # Use node object with constant oled_print(my_pin) # Display shows 'D13' COMMON ALIASES (many names work for same node): "TOP_RAIL" = "T_R" "GPIO_1" = "GPIO1" = "GP1" "DAC0" = "DAC_0" "UART_TX" = "TX" NOTES: - String names are case-insensitive: "d13" = "D13" = "nAnO_d13" - Constants are case-sensitive: use D13, not d13 - All three methods work in any function ``` --- # Odds and Ends ` - **Encoder-based connections** - Use the clickwheel to scroll through and select nodes without touching the probe - **Current sensing marching ants** - Animated visual feedback showing current flow direction between I+ and I- connections - **Python context switching** - Toggle between `global` and `python` connection contexts in the MicroPython REPL The JumperlOS firmware repo is at [https://github.com/Architeuthis-Flux/JumperlOS](https://github.com/Architeuthis-Flux/JumperlOS) --- --> ## Safety Info Here's an image of the little card that should have been inside your box [Image: Safety info Never put voltages above +9V or below -9V anywhere on this board. Don't use unpowered, the crossbars need power to block voltage too. Don't power externally, use the internal power supplies (rails / DACs). It can be powered from the 5V and GND pins on the Nano header or the FPC adapter instead of USB. External signals are okay, as long as the board remains powered.This board gets fairly warm in normal operation from the LEDs, if it ever gets hot, unplug it immediately and let me know. When the switch on the probe is set to Select Mode, it should only be used on the gold probe sense pads.The probe tip in Select Mode is always at 3.3V. Don't stab yourself or others with the probe, unless it's in self-defense. Do not eat your Jumperless V5. When in doubt, don't hesitate to ask!] - Never put voltages above +9V or below -9V anywhere on this board. - Don't use unpowered, the crossbars need power to block voltage too. - Don't power externally, use the internal power supplies (rails / DACs). - It can be powered from the 5V and GND pins on the Nano header or the FPC adapter instead of USB. - External signals are okay, as long as the board remains powered. - This board gets fairly warm in normal operation from the LEDs, if it ever gets hot, unplug it immediately and let me know. - When the switch on the probe is set to Select Mode, it should only be used on the gold probe sense pads. - The probe tip in Select Mode is always at 3.3V. - Don't stab yourself or others with the probe, unless it's in self-defense. - Do not eat your Jumperless V5. - When in doubt, don't hesitate to [ask!](https://discord.gg/bvacV7r3FP) There are a lot of exceptions to these if you know what you're doing. It's pretty hard to permanently damage this board. Some things (usually external power with the Jumperless off) can cause lockup on the [analog CMOS switches](https://tinyurl.com/24xrspea), but the current limiting resistors on their power supply pins generally keep them from drawing so much current that they permanently break. In situations where one chip is getting crazy hot, the first thing to try is to unplug the Jumperless, let it cool down, and try it again (obviously, change whatever you think was causing it). Most of the time they go back to normal after some rest. Don't let any of this scare you, I'd rather you just pretend it's indestructible and use it with reckless abandon. So if you manage to break anything, just let me know and I'll send out a fresh one and a return label, no questions asked*. *Actually, a ton of questions asked, so we can figure out how it happened and maybe prevent it from happening to someone else. But the point is I don't care if it's clearly your fault and not some manufacturing defect, I will make sure you have a working Jumperless. It's even printed on the box --- ## Joom You can get the .uf2 file here: [https://github.com/Architeuthis-Flux/joom/releases/download/0.0.1/joom_full.uf2](https://github.com/Architeuthis-Flux/joom/releases/download/0.0.1/joom_full.uf2) You'll need to put the Jumperless in bootloader mode (unplug it, press the button on the back side of the USB port, plug it back in, then drag this UF2 file onto the drive called `RP2350` that pops up) Connect a speaker between the lower `RST` pin (the one closer to the breadboard) on the Nano header and `GND` for sound. When you're done playing Doom in a blindness simulator, just reload the regular firmware the same way as above. [https://github.com/Architeuthis-Flux/JumperlessV5/releases/latest](https://github.com/Architeuthis-Flux/JumperlessV5/releases/latest) --- ## Bandwidth Michael has done some [awesome work characterizing the bandwidth of the Jumperless](https://codeberg.org/multiplex/jumperless-wigglyvolts). The TL;DR is just the physical breadboard puts the 3dB roll-off at ~13MHz, and a signal passing through the crossbar matrix brings it down to around ~8MHz. It makes sense these are pretty high, these CH446Qs were originally made for switching video signals so bandwidth was pretty important when they were designing them. Keep in mind this isn't a hard limit, it's just where the signal gets attenuated by the (arbitrarilyish) defined 3dB, so your signal's amplitude is reduced by √2. --- [Image: HeroNew] ## Animations The Jumperless uses LED animations to show the state of different components on the breadboard. ### Rail Animations If it's a rail, those are animated and should be a continuous slow pulsing toward the top or bottom depending on the rail. ### ADC Animations `ADCs` are green at 0V, and go through the spectrum to red at +5V, and get whiter hot pink toward +8V. Negative voltages are kinda blue/icy and do that same thing with the "cold" colors towards -8V. ### GPIO Animations ### Input Mode `GPIO` as inputs are animated with a white pulsing (this might be broken in that FW release, I'm fixing that right now actually, and will just be purple/white) when floating, red for high, green for low ### Output Mode `GPIO` outputs will be either green or red depending on their state --- ## What's that `BUFFER_IN - DAC_0` bridge that's always there? That gets added to power the `probe LEDs`, it's kinda weird, but to multiplex 3.3V, GND, LED data, 2 buttons, and a +-9V tolerant analog line over the 4 wires on a TRRS cable, the line powering those LEDs is shared. The `connect`/`measure` switch is a Dual Pole Dual Throw (DPDT) switch. The probe tip needs to be at a steady 3.3V to be read by the `probe sense pads` which is a big resistive divider sensed by a single `ADC`. When you have it in `select` mode, the probe tip is getting 3.3V from a `GPIO` on the RP2350B driven `high`, and the LEDs get their power from the analog line, which is `ROUTABLE_BUFFER_IN` connected to `DAC 0` set to 3.3V. When you switch to `measure` mode, those roles get swapped, the LEDs are powered by that `GPIO`, and the probe tip is now `ROUTABLE_BUFFER_IN`. In the current firmware, that just stays at 3.3V so you can *kinda* sense pads in either mode (you may notice the sensing is a lot wonkier, that's because the `DAC` isn't perfectly calibrated to output *exactly* 3.3V.) But in the future, there will be some other stuff you can do in that mode treating it as an analog line (and of course, I'll forget to update this, if it's after like June 2025, double check this is still true.) ##### A side effect of needing a crossbar connection to light the probe is that the LEDs in `Select` Mode act as a test of whether the Jumperless is properly making connections. ### Why am I using one of the precious two DACs and not another GPIO? The answer is switch position sensing. You may notice there's no obvious way for the Jumperless to know where the switch is set, so I had to get creative on this one. `DAC 0`'s output is hardwired to go through a `current sense` shunt resistor, so when `DAC 0` is powering the `probe LEDs`, they'll be drawing some current I can measure with one of the `INA219`s, and therefore I can be reasonably confident that the switch is in the `select` position. If you need both `DAC`s, you can just get rid of this connection and the `probe LEDs` won't light up, but other than aesthetics, it really has no effect on functionality. Or you connect `ROUTABLE_BUFFER_IN` to a `GPIO` and set it `high` and just lose the ability to sense where the switch is. --- ## AI Generated Wiki If you want to read a wiki generated by AI and ask it questions about how this thing works and how to use it, [**DeepWiki**](https://deepwiki.com/Architeuthis-Flux/JumperlessV5/1-overview) was surprisingly accurate (enough.) The docs on this site are more about how to *use* your Jumperless, this is more geared toward helping understand the circuitry and code. --- ## Onboard Help Use `help` or `[command]?` for onboard documentation [Image: Screenshot 2025-07-04 at 5 52 32 PM] --- ## [GitHub Releases](https://github.com/Architeuthis-Flux/JumperlessV5/releases) If you want more info about each feature when I was particularly excited about it, I usually write about the new features in the [Release notes on Github](https://github.com/Architeuthis-Flux/JumperlessV5/releases). --- ## Schematic Here's the schematic that's printed on the inner flap of the box If you want look at the schematic and PCB together and don't feel like downloading the whole thing and opening it in KiCad, [you can open it in the browser with KiCanvas here](https://kicanvas.org/?github=https://github.com/Architeuthis-Flux/JumperlessV5/blob/main/Jumperless23V50/MainBoard/JumperlessV5r6/JumperlessV5r6.kicad_pro) ## Writing Native apps Writing Apps ### Here's a the example app that should show the calls for most of the things you might want to do You can do literally anything the Jumperless can in an app, so if there's a specific thing, lmk and I'll write an example. Until I make this into a proper operating system, what you're doing when you write an App is just writing a function in the main firmware. There's really no guard rails, and the API is just any function in the firmware. ## First get it PlatformIO set up to flash code So fork the firmware here: https://github.com/Architeuthis-Flux/JumperlOS I'm using PlatformIO in VSCode. And it *should* just work to open the RP23V50firmware folder in that (you'll probably need to comment out `upload_port = /dev/cu.usbmodemJLV5port1` in `Platformio.ini` so it'll just automatically find it) You should probably try to just load the firmware just to make sure everything works. ## To write an App Before you go writing your app, follow these steps to make it so it's listed in the App library and you can run it from the menus. - Go to [`menuTree.h`](https://github.com/Architeuthis-Flux/JumperlessV5/blob/main/RP23V50firmware/src/menuTree.h) and add the name of your app under `Apps\n\` (shown as `-Custom App\n\` here, it needs to fit in 7x2 chars to show on the breadboard) - Go to `Apps.h` and declare your function where you'll write your app - Go to `Apps.cpp` and add a struct in the `struct app apps[30]` for your app `{"Name", index, ??idk, name of the function (unused)}` - Go to `Apps.cpp > runApp()` and add a `case` for your app's index (this is so you can also find it by index rather than exact matching the name `"Custom App"` - Make a function that's the entirety of your app, I just pushed a demo function called `customApp(void)` with some (non exhaustive) examples of things you can do from an app. - Run your app with the clickwheel, `Apps > Custom App`. The quick way run `"Custom App"` is to just enter `2` in the main menu, or just use the clickwheel and go Apps > Custom App. If you want to add your own shortcut, find an unused menu character and add a ``` case'3': { runApp(3); //the app index you set above break; } ``` in the big main menu `switch` statement in `main.cpp`. ## To actually write the app [The code](https://github.com/Architeuthis-Flux/JumperlessV5/blob/6fd4fcba572c4b524435ec36c8901adcedbf52c6/RP23V50firmware/src/Apps.cpp#L141) for `Custom App` is an example of the calls available with comments telling you what's going on. There are tons more, but what's shown there are the higher-level helper functions that should roughly do what they say they're doing. # LLM Tool Specification for Jumperless V5 A guide for LLMs with some tips to control the Jumperless V5. --- ## Quick Reference | Category | Key Tools | |----------|-----------| | **Connections** | `connect()`, `disconnect()`, `nodes_clear()`, `save_slot()`, `load_slot()` | | **Voltage** | `dac_set()`, `adc_get()` | | **Current** | `ina_get_current()`, `ina_get_power()` | | **GPIO** | `gpio_set()`, `gpio_get()`, `gpio_set_dir()`, `pwm()` | | **User Interaction** | `oled_print()`, `probe_read_blocking()`, `probe_button()` | | **Graphic Overlays** | `overlay_set()`, `overlay_clear()`, `overlay_set_pixel()` | | **State** | `get_state()`, `set_state()` | (Refer to the full [Micropython API Reference](09.5-micropythonAPIreference.md)) --- ## Communication Methods ### Method 1: Direct Python Commands (Main Serial Port) Prefix single-line Python with `>` on the main serial port (Port 1). *Best for: Single commands, status checks.* ``` > connect(1, 5) > voltage = adc_get(0) > oled_print(f"V = {voltage:.2f}") > print(f"V = {voltage:.2f}") ``` ### Method 2: ViperIDE / Raw REPL (3rd USB Port) The **third USB port** provides a MicroPython Raw REPL. *Best for: Complex logic, loops, automated testing scripts.* ```python # Full scripts run on port 3 import time for i in range(10): voltage = adc_get(0) print(f"Reading {i}: {voltage:.2f}V") time.sleep(0.5) ``` ### Method 3: Arduino Tags (via UART) From an Arduino connected to the Jumperless. *Best for: Hybrid Arduino/Python projects.* ```cpp Serial.print("connect(1, 5)"); // Python command Serial.print("n"); // Menu command ``` ### Method 4: Single-Character Commands (Main Serial Port) Raw characters sent to Port 1 trigger immediate menu actions. *Best for: Fast state dumps, clearing the board, or manual resets.* ``` J ` Python commands | | 2 | JLV5port3 | Arduino UART passthrough | | 3 | JLV5port5 | MicroPython Raw REPL (ViperIDE) | Windows: | Port | Name | Function | |------|------|----------| | 1 (main) | COM1 | Main terminal, menu, `>` Python commands | | 2 | COM2 | Arduino UART passthrough | | 3 | COM3 | MicroPython Raw REPL (ViperIDE) | --- ## Hardware Overview ### Physical Layout - **60 breadboard rows** (1-60) with 5 RGB LEDs underneath each - **Arduino Nano header** with routable pins - **OLED display** (128x32, optional but recommended) - **Probe** with touch-sensing tip, 2 buttons, mode switch - **Clickwheel** rotary encoder with button - **12 CH446Q crossbar chips** (A-L) for routing (~80Ω per path) ### Power - **TOP_RAIL / BOTTOM_RAIL**: Main power rails (±8V, 300mA) - **DAC0 / DAC1**: Auxiliary voltage outputs (±8V, 300mA each) - DAC0 connects to Probe Tip & INA0 - DAC0 and DAC1 are 0-3.3V native but amplified to ±8V - **Current Limits**: ~300mA per rail/DAC ### Measurement - **ADC0-3**: 4 user analog inputs (±8V range) - **INA0**: High-side current monitor on DAC0 (Probe Tip) - **INA1**: High-side current monitor on TOP_RAIL (configurable) ### GPIO - **10 GPIO pins** (RP2350B, 3.3V logic) Defined as GPIO_1 - GPIO_8 (physical gpio 20-27 on RP2350B), and UART_TX (gpio 0 on RP2350B) and UART_RX (gpio 1 on RP2350B) - **5V Tolerant Inputs**: Yes - **PWM**: Hardware PWM 0.1Hz-62.5MHz on all pins --- ## Node Addressing ### Breadboard Rows `1` through `60` ### Power Rails | Node | Description | |------|-------------| | `TOP_RAIL` | Top power rail (default 5V) | | `BOTTOM_RAIL` | Bottom rail (default GND) | | `GND` | Ground reference | | `DAC0` | DAC0 (connected to probe tip and INA0) | | `DAC1` | DAC1 (8V tolerant) | ### Arduino Pins `D0`-`D13`, `A0`-`A7`, `AREF`, `RESET` ### GPIO `GPIO_1`-`GPIO_8`, `UART_TX`, `UART_RX` ### ADC/Current Sense `ADC0`-`ADC3`, `ISENSE_PLUS`, `ISENSE_MINUS` --- ## Core Tool Definitions ### Connections & Slots ```jython connect(node1, node2, duplicates=-1) # Create connection disconnect(node1, node2) # Remove connection nodes_clear() # Remove ALL connections is_connected(node1, node2) # Check if connected # Slot Management save_slot(slot_id) # Save current state to slot 0-7 load_slot(slot_id) # Load state from slot 0-7 get_current_slot() # Returns active slot number # JSON State API (Recommended for LLMs) get_state() # Get complete state as JSON string set_state(json, clear_first=True, from_wokwi=False) # Apply state from JSON string # set from_wokwi=True to convert Wokwi diagram.json ``` ### Voltage Control ```jython dac_set(channel, voltage, save=True) # Set voltage (-8V to +8V) dac_get(channel) # Get current setting # Channels: 0/DAC0, 1/DAC1, 2/TOP_RAIL, 3/BOTTOM_RAIL ``` ### Measurement ```jython adc_get(channel) # Read ADC voltage (channels 0-3) ina_get_current(sensor) # Read current in Amps (0=DAC0/Probe, 1=TOP_RAIL) ina_get_voltage(sensor) # Read INA bus voltage ina_get_power(sensor) # Read power in Watts ``` ### GPIO & PWM ```jython gpio_set(pin, value) # Set output (True=3.3V, False=0V) gpio_get(pin) # Returns HIGH, LOW, or FLOATING gpio_set_dir(pin, direction) # True=OUTPUT, False=INPUT gpio_set_pull(pin, pull) # 1=PULLUP, -1=PULLDOWN, 0=NONE pwm(pin, frequency, duty) # Start PWM (duty: 0.0-1.0) pwm_stop(pin) # Stop PWM ``` ### Waveform Generator (WaveGen) ```jython # Setup wavegen_set_output(channel) # 0=DAC0, 1=DAC1 (Default) wavegen_set_wave(type) # 0=Sine, 1=Square, 2=Tri, 3=Saw wavegen_set_freq(hz) # Frequency in Hz wavegen_set_amplitude(vpp) # Peak-to-Peak Voltage (e.g. 3.3) wavegen_set_offset(volts) # DC Offset (e.g. 1.65) # Control wavegen_start(1) # Start output wavegen_stop() # Stop output ``` ### User Interaction ```jython oled_print(text, size=2) # Display on OLED oled_clear() # Clear display probe_read_blocking() # Wait for probe touch, return row probe_read_nonblocking() # Check without waiting (-1 if none) probe_button() # Returns CONNECT, REMOVE, or NONE clickwheel_get_direction() # Returns UP, DOWN, or NONE clickwheel_get_button() # Returns PRESSED, HELD, RELEASED ``` ### Graphic Overlays (Breadboard LEDs) The breadboard LEDs are addressed as a 10x30 grid (Row 1-10, Col 1-30). Rows 1-5 are top half (E-A), Rows 6-10 are bottom half (F-J). ```jython # overlay_set(name, x, y, height, width, colors) # Colors can be flat list or 2D list of 0xRRGGBB integers overlay_set("box", 1, 1, 5, 5, [0x550000]*25) overlay_clear("box") # Remove overlay overlay_clear_all() # Remove all overlay_set_pixel(x, y, color) # Set single pixel (1-30, 1-10) ``` ### System & Filesystem ```jython # Standard Python I/O is supported! with open('/config.txt', 'r') as f: print(f.read()) # List files import os os.listdir('/') ``` ```jython get_net_info(netNum) # Get dict with name, color, nodes get_num_nets() # Count of active nets get_num_bridges() # Count of bridges print_bridges() # Print bridge table ``` ### Single-Character Command Reference (Port 1 Only) These commands are processed immediately when sent as raw characters (no `>` prefix) to the main serial port. | Char | Description | Action | |------|-------------|--------| | `J` | **Show JSON** | Dumps the complete board state as a JSON string. | | `L` | **Load JSON** | Prepares the board to receive a JSON state. Paste JSON and end with an empty line. | | `x` | **Clear All** | Removes all connections and resets paths. | | `n` | **List Nets** | Prints a human-readable list of all active nets. | | `b` | **Show Bridges**| Prints the internal bridge array. | | `~` | **Show Config** | Dumps the current `config.txt` settings. | | `+` | **Add** | Add connections (e.g., `+1-5,10-12`). | | `-` | **Remove** | Remove connections (e.g., `-1-5`). | | `v` | **Read ADC** | Follow with a channel (0-4) to get a quick voltage reading. | | `@` | **I2C Scan** | Scans for I2C devices on a row (e.g., `@10`). | | `r` | **Reset Arduino** | Follow with `t` or `b` to reset the Top or Bottom Arduino. | | `A` | **Connect Arduino UART** | Connects Jumperless's UART to the Arduino D0 and D1 pins (`a` to disconnect). | | `m` | **Menu** | Displays the help menu (`e` to show more options). | | `[command]?` | **Help** | Displays the help menu for the specified command. | | `help` | **Help Menu** | Displays the help menu. | --- ## LLM Mental Model File Format LLMs should maintain a persistent JSON model of what they believe is physically on the breadboard. This model has **confidence values** that increase through user confirmation or automated testing. ### Mental Model Schema ```json { "version": "1.0", "last_updated": "2026-02-06T22:00:00Z", "nano_header": { "device": "arduino_nano", // "arduino_nano", "rp2040", "rpi_40pin_adapter", "oled_only", "empty" "confidence": 0.9, "notes": "User confirmed Arduino Nano Every" }, "power_rails": { "TOP_RAIL": {"voltage": 5.0, "confidence": 1.0, "source": "measured"}, "BOTTOM_RAIL": {"voltage": 0.0, "confidence": 1.0, "source": "measured"} }, "components": [ { "id": "comp_001", "type": "resistor", "value": 1000, "unit": "ohms", "tolerance": 0.05, "pins": [5, 10], "confidence": 0.95, "detection_method": "measured", "notes": "Measured 987Ω between rows 5-10" }, { "id": "comp_002", "type": "led", "color": "red", "forward_voltage": 1.8, "pins": {"anode": 15, "cathode": 16}, "confidence": 0.7, "detection_method": "user_stated", "notes": "User said 'red LED on rows 15-16'" }, { "id": "comp_003", "type": "module", "name": "SSD1306 OLED", "pins": { "GND": 20, "VCC": 21, "SCL": 22, "SDA": 23 }, "confidence": 0.6, "detection_method": "inferred", "notes": "Searched pinout, user confirmed row 20" } ], "wires": [ {"from": 1, "to": 30, "confidence": 0.8, "detection_method": "continuity_test"} ], "unknowns": [ {"rows": [40, 41, 42], "notes": "Something detected but not identified"} ] } ``` ### Nano Header Device Types | Device | Description | |--------|-------------| | `arduino_nano` | Arduino Nano/Every/RP2040 etc. | | `rp2040` | Bare RP2040 board | | `rpi_40pin_adapter` | RPi GPIO adapter board | | `oled_only` | SBC/SMD/OLED adapter for just the OLED | | `empty` | Nothing plugged in | ### Confidence Levels | Level | Source | Meaning | |-------|--------|---------| | 1.0 | `measured` | Electrically verified | | 0.9 | `user_confirmed` | User explicitly confirmed | | 0.7 | `user_stated` | User mentioned it casually | | 0.5 | `inferred` | LLM guessed from context | | 0.3 | `assumed` | Default assumption | --- 0.001: # > 1mA flows resistance = v_drop / current if resistance 0.1: # Conducting vf = v - measured if vf 0.1: return {"type": "wire", "confidence": 0.9} # Conducts both ways # 4. Capacitance Test (RC time constant) # Use internal resistance + measure charge time # ... (implementation depends on available timing) return {"type": "unknown", "confidence": 0.0} ``` ### Test Sequence for Unknown Board When a user says "I have some stuff on the board but I'm not sure what": ```jython def scan_all_rows(): """Scan all 60 rows for connections and components.""" connections = [] for row_a in range(1, 60): for row_b in range(row_a + 1, 61): result = detect_component(row_a, row_b) if result["type"] != "unknown": connections.append({ "rows": [row_a, row_b], **result }) oled_print(f"{row_a}-{row_b}: {result['type']}") return connections ``` --> ### Increasing Confidence | Method | Confidence Boost | |--------|------------------| | User confirms "yes that's right" | +0.2 | | Electrical measurement matches | +0.3 | | Multiple consistent tests | +0.1 per test | | User provides datasheet | Set to 0.95 | --- ## Workflows for LLMs ### 1. Circuit Safe-Check (Mental Model) **CRITICAL**: The Jumperless prevents internal shorts, but **cannot see external wires or components** on the breadboard. If a user has a physical wire connecting Row 5 to Row 10, connecting `TOP_RAIL->5` and `GND->10` will cause a dead short through the user's wire. **Workflow:** 1. **Ask**: "What is currently on the breadboard? (Wires, components, etc)" 2. **Model**: Build the mental model JSON with low confidence 3. **Test**: Run `detect_component()` on key rows to verify 4. **Update**: Increase confidence as tests confirm the model ```jython # Safe Power-Up Sequence oled_print("Checking for shorts...") # Find an unused ADC unused_adc = None for i in range(4): name = f"ADC{i}" is_used = False for net in get_all_nets(): if name in net['nodes']: is_used = True break if not is_used: unused_adc = i break if unused_adc is not None: connect(5, f"ADC{unused_adc}") connect(10, DAC1) # ... ``` model["wires"].append({"from": 5, "to": 10, "confidence": 0.95}) else: connect(5, TOP_RAIL) connect(10, GND) ``` ### 2. Identifying User Circuits Use the Probe to "see" the board. ```jython oled_print("Touch the input of your circuit") input_row = probe_read_blocking() oled_print(f"Input is at row {input_row}") ``` ### 3. Automated Characterization Use WaveGen and ADC to treat the circuit as a black box. ```jython connect(DAC1, 1) wavegen_set_output(1) wavegen_set_freq(100) wavegen_set_wave(0) # Sine wavegen_start(1) connect(ADC0, 10) for i in range(5): val = adc_get(0) print(f"Sample {i}: {val}V") time.sleep(0.01) ``` --- ## Component Library (Dynamic Lookup) When a user mentions a hardware module, **search the web for its pinout** and build a JSON definition on the fly. **Workflow:** 1. User mentions: "I have an SSD1306 OLED on the breadboard" 2. LLM searches: "SSD1306 OLED pinout" 3. LLM builds JSON from search results and adds it to `mental_model["components"]` **Example: NeoPixel Stick** ```json { "name": "NeoPixel Stick 8", "pins": { "GND": {"default": "GND", "offset": 0}, "5V": {"default": "TOP_RAIL", "offset": 1}, "DIN": {"default": "GPIO_1", "offset": 2}, "DOUT": {"default": "NC", "offset": 7} }, "width": 8, "voltage": "5V", "notes": "3.3V GPIO works for most NeoPixels. Data on offset 2." } ``` **Key**: The `offset` field defines pin position relative to pin 1. When user says "pin 1 is on row X", calculate absolute rows as `row = X + offset`. --- ## Safety Guidelines for LLMs 1. **Voltage Check**: ADCs are buffered for ±8V. The board is ±9V tolerant overall. 2. **Short Circuit Prevention**: The firmware will ignore requests to connect `TOP_RAIL` directly to `BOTTOM_RAIL` or `GND`. 3. **Confirm Power**: Ask: "Is the board powered via USB?" (No barrel jack exists). 4. **Crossbar Resistance**: Remember ~80Ω per connection. High current paths will have voltage drop. Measure voltage *at the destination* with an ADC to compensate. --- ## LLM Preferences (Claude's Additions) ### 1. Structured State Snapshot **What I want**: A single command that returns the complete board state as structured data (JSON/dict), not just printed text. This lets me reason about the state programmatically. ```python # REQUESTED: get_state() -> dict # Returns something like: { "slot": 0, "bridges": [[1, 5], [5, "TOP_RAIL"], [10, "GND"]], "rails": {"TOP_RAIL": 5.0, "BOTTOM_RAIL": 0.0, "DAC0": 3.3, "DAC1": 0.0}, "gpio": [ {"pin": 1, "dir": "OUTPUT", "value": True, "pull": "NONE"}, {"pin": 2, "dir": "INPUT", "value": False, "pull": "PULLUP"} ], "adc_snapshot": [3.28, 0.01, 5.02, -0.03] # Quick reading of all 4 } ``` **Why**: Currently I have to call `get_num_bridges()`, `get_net_info()` for each net, etc. A single snapshot is faster and less error-prone for building my mental model. ### 2. Return Values, Not Just Prints For debugging, I prefer **return values** over **print statements**: | Instead of... | I prefer... | |---------------|-------------| | `print_nets()` → prints to serial | `get_nets()` → returns list of net dicts | | `print_bridges()` → prints to serial | `get_bridges()` → returns list of bridge tuples | | `print_paths_compact()` → prints | `get_paths()` → returns routing info | **Why**: When I call a tool, I want to capture the result and reason about it. Print output goes to the user's terminal but isn't easily parsed by my next step. ### 3. Error Return Conventions Consistent error handling helps me recover: ```python # Good: Returns None or raises exception with message result = connect(999, 5) # Invalid node # Returns: None (or {"error": "Invalid node: 999"}) # Good: Returns success/failure boolean with reason success, msg = disconnect(1, 5) # Returns: (True, "Disconnected") or (False, "No such connection") ``` ### 4. Undo via Slot Backup For destructive operations like `nodes_clear()`, the existing slot system provides an undo mechanism: ```python # Before destructive operation, save current state to a backup slot save_slot(7) # Save to slot 7 as backup nodes_clear() # Now safe to clear # If user wants to undo: load_slot(7) # Restore from backup ``` **Pattern**: Always save the current slot to an unused slot (7 is a good "scratch" slot) before any destructive action. This provides a built-in undo without needing special confirm flags. ### 5. Measurement with Context When measuring, I often want multiple samples or statistics: ```jython # REQUESTED: adc_get_stats(channel, samples=10) # Returns: {"mean": 3.28, "min": 3.25, "max": 3.31, "stddev": 0.02} ``` **Why**: A single ADC reading might be noisy. Having built-in averaging/stats means I don't have to write loops for every measurement. ### 6. Interactive Conversation Patterns When helping users debug, I find these patterns effective: **Explore First, Act Later**: ``` User: "My LED isn't lighting up" Me: 1. "Where is your LED connected? (Touch the anode with the probe)" 2. [probe_read_blocking() → row 15] 3. "I see row 15. Let me check the voltage there..." 4. [connect(ADC0, 15), adc_get(0) → 0.02V] 5. "Row 15 is at 0V. Is it supposed to be connected to power?" ``` **Show, Don't Just Do**: ```jython # Before making a connection, describe it: oled_print("Connecting row 5 to 5V...") connect(5, TOP_RAIL) oled_print("Done! LED should light now") ``` **Verify After Acting**: ```jython # After connecting power, verify it worked: connect(5, TOP_RAIL) connect(ADC0, 5) v = adc_get(0) if abs(v - 5.0) 0.1: dac_set(0, 3.3) time.sleep(0.01) # Allow settling new_val = dac_get(0) if abs(new_val - 3.3) > 0.1: print(f"Error: DAC0 failed to set. Got {new_val}V") ``` ### 2. Structured State Snapshot A single command to return the complete board state as a formatted JSON string, enabling detailed programmatic reasoning and full state management. ```jython # Get the complete current state as a formatted JSON string snapshot = get_state() # The snapshot includes: # - power: Settings for TOP_RAIL, BOTTOM_RAIL, DAC0, DAC1 # - nets: All active connections, names, colors, and voltage assignments # - gpio: Current configuration and state of all GPIO pins # Apply a state back to the hardware # set_state(json_string, clear_first=True) # If clear_first=True (default), it resets the board before applying set_state(snapshot) ``` ### 3. Search-First Component Handling Ground knowledge by searching for pinouts *before* asking the user. **Workflow:** 1. User: "I have a BME280." 2. Agent: `search_web("BME280 pinout SPI I2C")` 3. Agent: "I see the BME280 supports both SPI and I2C. Which one are you using?" ### 4. Batch Operations Process information efficiently in large chunks to reduce round-trips and ensure atomic updates. The recommended way to perform complex batch reconfigurations is to fetch the current state, modify it in Python, and re-apply it. ```jython # Recommended Batch Workflow: state_json = get_state() state = json.loads(state_json) # 1. Modify connections state['nets'].append({"index": 10, "name": "SIGNAL", "nodes": [5, 12, "D7"]}) # 2. Update power settings state['power']['top_rail'] = 3.3 # 3. Configure GPIO state['gpio'][0]['dir'] = "OUTPUT" state['gpio'][0]['value'] = True # 4. Apply all changes at once set_state(json.dumps(state)) ``` ### 5. Return Values Over Prints Tools should return data structures (lists, dicts) for programmatic use, not just print to stdout. ### 6. undo via Slot Backup Always save the current state to a scratch slot (e.g. slot 7) before destructive operations like `nodes_clear()`. ### 7. Context-Aware Error Recovery Error messages should include `suggested_fix` fields to allow self-correction without user intervention. ### 8. Interactive Conversation Patterns **Explore First, Act Later**: Probe and measure *before* applying power to unknown circuits. **Show, Don't Just Do**: Explain actions via `oled_print()` and `print()` before executing them to keep the user informed. --- ## Implementation Status | Feature | Status | |---------|--------| | `connect()`, `disconnect()`, `is_connected()` | ✅ Implemented | | `dac_set()`, `adc_get()`, `ina_*()` | ✅ Implemented | | `gpio_*()`, `pwm()` | ✅ Implemented | | `oled_print()`, `probe_*()` | ✅ Implemented | | WaveGen tools | ✅ Implemented | | Slot management | ✅ Implemented | | `get_state()` / `set_state()` snapshot | ✅ Implemented | | Slot backup for undo | ✅ Implemented (use `save_slot(7)` before destructive ops) | --- # 3D Printable Stand I consider this an essential part of using a Jumperless and if they could fit in the box, I would've included them. So print a stand or ask someone to print it for you. Spin the model below to have a look and switch between variants. Grab the **STL** to print right away, or the **STEP** if you'd rather tweak the design (and let me know about any improvements you make and I'll add it here): [Here are the 3D models on Printables,](https://www.printables.com/model/1249365-jumperless-stand) if you prefer that for whatever reason. [[Image: 3DStandCollage]](https://www.printables.com/model/12493x65-jumperless-stand) These all should print fine without supports, the angles of the probe slot were designed so there aren't any flat overhangs, but you do you. ## Rubber Feet These [stick-on rubber feet](https://www.amazon.com/AmazonBasics-300-Piece-Adhesive-Rubber-Bumpers/dp/B087MG3G76) also make it a lot more solid on your desk (and having the different sizes lets you shim the angle by putting different ones on the front and back.) If you were wondering why there's an little square of sticky feet in your box under the FPC board, this is why. --- # Developer Guide Write apps for your conference badge using Python. This guide covers everything from connecting JumperIDE to building multi-file games with LED matrix animations and badge-to-badge IR communication. For the exhaustive function-by-function reference, see the [API Reference](badge-api-reference.md) (also available on-device at `/API_REFERENCE.md`). --- ## 1. What Is This Badge? The Temporal Badge is a wearable conference badge built on the **ESP32-S3-WROOM-1 16N8** module (16 MB flash, 8 MB PSRAM). It runs Arduino C++ firmware with an embedded **MicroPython v1.27** runtime, so you can write Python apps that control all the hardware directly. ### Hardware at a Glance | Component | Spec | |-----------|------| | MCU | ESP32-S3-WROOM-1 16N8 (dual-core, 240 MHz, 8 MB PSRAM, 16 MB flash) | | Display | 128×64 monochrome OLED (SSD1306) | | LED Matrix | 8×8 red LEDs (IS31FL3731, PWM per pixel) | | Input | 4 d-pad buttons + analog joystick | | Motion | LIS2DH12 3-axis accelerometer | | IR | NEC-protocol TX LED + TSOP receiver | | Haptics | Vibration motor with coil-tone support | | Storage | FatFS `ffat` partition (0x600000 bytes in `partitions_replay_16MB_doom.csv`, mounted at `/apps/`) | | Python heap | 128 KB from PSRAM | ### Physical Layout The badge is held **upright** during use. When idle (walking around the conference), it rests **upside down** on the lanyard, and the firmware automatically flips the display to show a nametag. [Image: Badge photo] ``` ╭───────────────────────────────────────────────╮ │ ▀▀ IR TX/RX ▀▀ │ │ │ │ ╭───────────────────────────────────────╮ │ │ │ │ │ │ │ 128×64 OLED Display │ │ │ │ (0,0) ───► x │ │ │ │ │ │ │ │ │ ▼ y │ │ │ │ │ │ │ ╰───────────────────────────────────────╯ │ │ │ │ ╭───────────╮ │ │ │ · · · · · │ [Y] │ │ ◉ │ · 8×8 · │ [X] [B] │ │ Joystick │ · LED · │ [A] │ │ │ · Matrix· │ │ │ │ · · · · · │ │ │ ╰───────────╯ │ ╰───────────────────────────────────────────────╯ Buttons: [Y] = BTN_UP / BTN_TRIANGLE [X] = BTN_LEFT / BTN_SQUARE [B] = BTN_RIGHT / BTN_CIRCLE [A] = BTN_DOWN / BTN_CROSS ``` **Button mapping:** - Physical: `BTN_UP` (Y), `BTN_DOWN` (A), `BTN_LEFT` (X), `BTN_RIGHT` (B) - PlayStation aliases: `BTN_TRIANGLE`, `BTN_CROSS`, `BTN_SQUARE`, `BTN_CIRCLE` - Semantic: `BTN_CONFIRM` (select/OK), `BTN_BACK` (cancel/back) — these follow the user's confirm/back swap setting ### Orientation and Nametag Mode The IMU detects when the badge is flipped upside down. The firmware automatically: - Flips the OLED to show an idle display / nametag - Rotates button and joystick input to match the new orientation - Flips the LED matrix Your app can detect this too — see [Flip/Nametag Detection](#flipnametag-detection) in the Advanced Topics section. --- ## 2. Getting Started with JumperIDE The fastest way to write and test badge code is **JumperIDE**, a browser-based MicroPython IDE that connects over WebSerial. Go to [https://ide.jumperless.org/](https://ide.jumperless.org/) and press the **Connect** button. Select the badge serial port from the browser picker. - On macOS this usually appears as USB/JTAG serial for the ESP32-S3. - On Windows this appears as a COM port. - If multiple ports appear, try the one that shows the MicroPython REPL prompt. Open or create a script, then hit **Run / Stop** (or press `F5`). Press it again to stop. If you make edits, hit the green **Save** button (`Ctrl+S`) to write the file to the badge filesystem. The REPL terminal at the bottom shows `print()` output and exceptions. JumperIDE uses MicroPython raw REPL under the hood, so anything compatible with raw REPL workflows (including `mpremote`) also works with the badge. **Tip:** type `o` and Enter at the REPL to dump a block-art screenshot of the current OLED and LED matrix to the terminal — handy while debugging UI or capturing reference output for docs. See [Serial screenshots](#serial-screenshots-oled--led). ### File Management JumperIDE shows the badge filesystem in a tree view. You can: - Browse `/apps/`, `/lib/`, `/tests/` - Open and edit files directly on the badge - Create new files and directories - Save changes with `Ctrl+S` ## 3. Your First App ### Hello World ```jython import time oled_clear() oled_set_cursor(32, 28) oled_print("Hello Badge!") oled_show() led_show_image(IMG_HEART) time.sleep_ms(3000) led_clear() exit() ``` This clears the OLED, prints centered text, shows a heart on the LED matrix for 3 seconds, then exits back to the menu. ### Adding Input ```jython import time count = 0 while True: oled_clear() oled_set_cursor(0, 0) oled_print("Press buttons!") oled_set_cursor(0, 20) oled_print("Count: " + str(count)) oled_show() if button_pressed(BTN_CONFIRM): count += 1 haptic_pulse() led_show_image(IMG_SMILEY) if button_pressed(BTN_BACK): break time.sleep_ms(30) led_clear() exit() ``` **Key patterns:** - `button_pressed()` is edge-triggered — it returns `True` once per press, then `False` until the button is released and pressed again. Use this for menu navigation. - `button()` is level-triggered — it returns `True` as long as the button is held. Use this for continuous actions (shooting, accelerating). - Always call `time.sleep_ms(20-30)` in your main loop to yield CPU time. - Call `oled_show()` after drawing to make changes visible. ### Using the Native UI Chrome For apps that should look like the built-in firmware screens, use `badge_ui`: ```jython import badge_ui as ui import time ui.chrome("My App", "v1.0", "OK", "action", "BACK", "quit") ui.line(0, "Hello from Python!") ui.line(1, "This matches the firmware style") oled_show() while True: if button_pressed(BTN_BACK): break time.sleep_ms(30) exit() ``` `badge_ui` calls the native C++ UI layout code, so your app's header, footer, and button glyph icons are pixel-identical to the firmware menus. See `initial_filesystem/lib/badge_ui.py` for all available helpers. --- ## 4. App Structure ### Single-File Apps Good for quick experiments and small demos. Place a `.py` file in `/apps/`: ``` /apps/my_demo.py ``` All `badge` module functions are auto-imported into the global scope — no `import badge` needed in single-file apps. ### Multi-File Apps For anything beyond a simple demo, use a folder with a `main.py` entry point: ``` /apps/my_game/ main.py # Entry point (tiny — just imports and calls main) engine.py # Game loop and logic data.py # Constants, level data screens.py # OLED rendering functions icon.py # Optional: app icon bitmap for the menu ``` The `main.py` should be minimal: ```jython """My Game app entry point.""" import sys APP_DIR = "/apps/my_game" if APP_DIR not in sys.path: sys.path.insert(0, APP_DIR) from engine import main main() ``` In your other modules, explicitly import what you need: ```jython from badge import * import time import gc ``` This pattern is used by BreakSnake, Flappy Asteroids, and Synth. The `sys.path.insert` lets Python find sibling modules in the app directory. ### Showing Up on the Main Menu Drop a folder with a `main.py` under `/apps/` and the badge picks it up automatically — both production and dev firmware. No C++ changes needed. The firmware text-scans the top of `main.py` for a few optional dunder assignments and uses them to decorate the main-menu tile: ```jython """My Game — Tamagotchi-style desk pet.""" __title__ = "My Game" __description__ = "A tiny pet that lives in your pocket." __icon__ = "icon.py" ``` | Dunder | Default | Notes | |--------|---------|-------| | `__title__` | slug, title-cased (`my_game` → `My Game`) | max 19 chars | | `__description__` | empty | max 63 chars; shown on the tile's detail panel | | `__icon__` | tries `icon.py` opportunistically | path to a 12×12 packed XBM tuple | | `__matrix_title__` | `__title__` | label in the MATRIX APPS picker | | `__order__` | `10000 + discovery index` | signed int; lower = earlier on grid | #### Icon File (`icon.py`) A 12×12 monochrome XBM, two bytes per row × 12 rows = 24 bytes. Bit 0 of each byte is the leftmost pixel (U8G2 `drawXBM` order). The high 4 bits of every odd byte are unused. ```jython """My Game icon.""" WIDTH = 12 HEIGHT = 12 # Two bytes per row, binary literals so the dots are visible in the # source. XBM byte order is LSB-first, so the literal reads mirrored # relative to the rendered icon — that's fine, equivalence at the bit # level is what matters. DATA = ( 0b01110111, 0b00000111, 0b01110111, 0b00000111, 0b00000000, 0b00000000, 0b01100000, 0b00000000, 0b01100000, 0b00000000, 0b11111100, 0b00000011, 0b00000000, 0b00000000, 0b00111110, 0b00000000, 0b00100000, 0b00000000, 0b11100000, 0b00000011, 0b00000000, 0b00000010, 0b11000000, 0b00000011, ) ``` If `__icon__` is omitted the firmware tries `/apps//icon.py` anyway, so most apps just need to ship a file with that name. #### Hot-Refresh After editing any of `main.py`, `icon.py`, or `matrix.py`, refresh the menu without rebooting: ```jython import badge badge.rescan_apps() ``` The dev firmware also surfaces a generic Apps screen that lists every `.py` under `/apps/` (single-file or folder), useful for one-off scripts you don't want on the main grid. #### Limits - 32 dynamic apps per badge. - Slugs must match `[A-Za-z0-9_-]+` and not start with `.`. - The dunder scanner only reads the first ~2 KB of `main.py`. Put your manifest at the very top of the file. #### Tile Order Three layers, each overriding the next, feed a signed-int sort key: 1. **Defaults.** Curated tiles use `10 × array index` (with `SETTINGS` pinned to `30000`), dynamic apps use `10000 + discovery index`. 2. **App manifest** (`__order__ = 50`). 3. **User override** stored in NVS by the manual reorder screen. The menu is then stable-sorted by that key, so duplicate values keep the placement order from the previous layer. Negative keys land *before* any curated tile; large keys land near `SETTINGS`. Inspired typical picks: ```jython __order__ = -10 # before BOOP __order__ = 25 # between MAP (30) and SCHEDULE (40) __order__ = 9999 # nearly last ``` #### User Reorder Players can rearrange the grid themselves via **Settings → Menu → Reorder**: | Button | Action | |--------|--------| | Joystick Y | Move cursor up/down | | `X` | Pick up / drop a row | | `A` (confirm) | Save and rebuild | | `B` (back) | Cancel | While picked up, joystick Y drags the row in real time. Save writes a per-label override into the `menu_order` NVS namespace; the rebuilder reads those overrides on the next refresh (immediate after save, and on every boot). **Settings → Menu → Reset Order** wipes the namespace and returns every tile to its default order. ### Persistent Matrix Apps Drop a `matrix.py` next to `main.py` and your app gets a slot in the **MATRIX APPS** picker (firmware menu → MATRIX APPS). Selecting it persists the choice in `/led_state.json` so the badge runs your matrix animation across reboots, **even when no foreground Python app is open**. The script registers a tick callback with [`matrix_app_start`](badge-api-reference.md#matrix-app-host) and returns immediately: ```jython """Slow drifting dot.""" __matrix_title__ = "Drift" import badge _phase = 0 def _tick(now_ms): global _phase _phase = (_phase + 1) & 7 frame = [0] * 8 frame[7] = 0x80 >> _phase badge.led_set_frame(frame) badge.matrix_app_start(_tick, 250, 24) ``` The callback runs from the firmware's matrix service pump — same context as any other `matrix_app_start` callback — so the same constraints apply: keep it fast, don't block, and don't sit in a `while True` loop. See [Section 6 → LED Matrix](#led-matrix-88) for details. A real example lives in `/apps/tardigotchi/matrix.py`: it reads `/tardigrade_save.json`, walks a growing pet glyph along the bottom rows, cycles heart/drumstick/face status icons across the top, and pulses the haptic motor for Tamagotchi-style beeps. Stat decay continues at 1/10 the foreground game's pace and is written back to the save file so the foreground app sees the ambient progress next time you launch it. Switching to any built-in mode (Sparkle, Off, etc.) — or to a different matrix app — cleanly stops the previous callback. There is no need to call `matrix_app_stop()` from your script; the firmware tears it down for you on mode change. ### Saving Data There are two ways to persist data, and the choice matters: **Use `badge.kv` for state that must survive a reflash** — game saves, high scores, user prefs. NVS is invariant across firmware updates, factory reflashes, and Community Apps installs. ```jython import badge # Read with a default; write any str/int/float/bytes value. hi = badge.kv_get("hi_breaksnake", 0) hi += 1 badge.kv_put("hi_breaksnake", hi) ``` Or via the friendlier wrapper baked into `/lib/badge_kv.py`: ```jython from badge_kv import kv kv.put("hi_breaksnake", kv.get("hi_breaksnake", 0) + 1) ``` Limits: 15 chars per key, 1 KB per value, 64 keys per badge. Supported value types: `str`, `int`, `float`, `bytes`. **Use the filesystem for replaceable content** — caches, downloaded data, large blobs over 1 KB. FATFS files survive a firmware-only reflash but are wiped by a `fatfs.bin` reflash or a JumperIDE "Sync Filesystem" with `--clear-extras`: ```jython # Fine — it's a cache that can be rebuilt. import json with open("/cache/last_query.json", "w") as f: f.write(json.dumps(result)) ``` > **Why this matters**: as of firmware v0.2, only `/lib` and > `/matrixApps` are baked into the firmware image. Everything else > (your app, docs, images, the DOOM WAD) ships via factory > `fatfs.bin` flash and can be re-pushed via Community Apps or > JumperIDE. State you care about belongs in NVS so it can't get > wiped by a reflash. See > [Storage Model](https://github.com/Architeuthis-Flux/Temporal-Replay-26-Badge/blob/main/firmware/docs/STORAGE-MODEL.md) > for the full survival matrix. --- ## 5. MicroPython Cheat Sheet ### Available Modules | Module | Notes | |--------|-------| | `sys` | `sys.path`, `sys.exit()` | | `os` | Filesystem: `listdir`, `mkdir`, `remove`, `stat` | | `time` | `sleep_ms()`, `ticks_ms()`, `ticks_diff()` | | `random` | `randint()`, `choice()`, `uniform()` | | `math` | `sin()`, `cos()`, `sqrt()`, `pi` | | `cmath` | Complex math | | `struct` | `pack()`, `unpack()` for binary data | | `array` | Typed arrays | | `binascii` | `hexlify()`, `unhexlify()` | | `json` | `loads()`, `dumps()` | | `collections` | `OrderedDict`, `namedtuple` | | `errno` | Error constants | | `gc` | `collect()`, `mem_free()`, `mem_alloc()` | | `io` | `StringIO`, `BytesIO` | | `micropython` | `mem_info()`, `stack_use()` | | `uctypes` | C-compatible struct access | | `badge` | All badge hardware (auto-imported) | ### Key Differences from CPython **f-strings are supported.** You can use normal MicroPython string formatting: ```jython score = 42 print(f"Score: {score}") print("Score: " + str(score)) # also fine ``` **Time functions use milliseconds:** ```jython import time time.sleep_ms(100) # 100 ms time.sleep(1) # 1 second (float OK) start = time.ticks_ms() # ... do work ... elapsed = time.ticks_diff(time.ticks_ms(), start) ``` Use a cooperative main loop. Long-running computation in the foreground loop can block input/render responsiveness, so keep per-frame work small and use `sleep_ms()` to yield. **Memory is limited.** The Python heap is 128 KB from PSRAM. Call `gc.collect()` periodically in long-running apps, especially after releasing large objects: ```jython import gc gc.collect() print("Free:", gc.mem_free(), "bytes") ``` **No pip / external packages.** Only the modules listed above are available. Shared code goes in `/lib/` (which is on `sys.path`). For the full MicroPython language reference: [docs.micropython.org](https://docs.micropython.org/en/latest/) --- ## 6. Hardware Guide ### OLED Display (128×64) The coordinate system has **(0, 0) at the top-left corner**. X ranges from 0–127, Y from 0–63. The display is buffered — draw to the buffer, then call `oled_show()` to push it to the screen. **Text rendering:** ```jython oled_clear() oled_set_cursor(0, 0) oled_print("Top-left") oled_set_cursor(0, 30) oled_set_text_size(2) oled_print("BIG") oled_set_text_size(1) oled_show() ``` **Centering text:** ```jython text = "Centered!" w = oled_text_width(text) x = (128 - w) // 2 oled_set_cursor(x, 28) oled_print(text) oled_show() ``` **Fonts:** ```jython fonts = oled_get_fonts().split(",") for f in fonts: oled_set_font(f) oled_clear() oled_set_cursor(0, 20) oled_print(f) oled_show() time.sleep_ms(500) ``` **Drawing primitives:** ```jython oled_set_pixel(64, 32, 1) # Single pixel oled_draw_box(10, 10, 50, 20) # Filled rectangle oled_set_draw_color(2) # XOR mode oled_draw_box(20, 5, 30, 30) # XOR overlay oled_set_draw_color(1) # Back to white oled_show() ``` **Framebuffer access** for advanced effects: ```jython fb = oled_get_framebuffer() # bytes object, 1024 bytes w, h, size = oled_get_framebuffer_size() buf = bytearray(fb) # Modify buf... oled_set_framebuffer(buf) oled_show() ``` **Drawing XBM bitmaps from Python.** The badge has no native `oled_draw_xbm` helper, but you can blit a 1-bit XBM by walking the bytes and calling `oled_set_pixel` for each lit bit. XBM is LSB-first within each byte, so bit 0 is the leftmost pixel: ```jython def draw_xbm(bits, offset, w, h, x0, y0): stride = (w + 7) // 8 for row in range(h): base = offset + row * stride for col_byte in range(stride): byte = bits[base + col_byte] if not byte: continue for bit in range(8): col = (col_byte = w: break if byte & (1 1000: long_press_action() ``` **Escape chord:** Holding all four face buttons for ~1 second force-exits any running app. This is a firmware safety net — you don't need to implement it. ### Joystick A 2-axis analog joystick returning raw ADC values (0–4095). Center position is approximately 2048, but varies per unit. ```jython x = joy_x() # 0 = full left, 4095 = full right y = joy_y() # 0 = full up, 4095 = full down # Dead zone handling CENTER = 2048 DEAD = 300 dx = x - CENTER dy = y - CENTER if abs(dx) = 3: return words[0], words[1], words[2] return None # Game loop while True: if button_pressed(BTN_CONFIRM): send_position(joy_x(), joy_y(), 42) pos = receive_position() if pos: x, y, score = pos oled_clear() oled_println("Peer: " + str(x) + "," + str(y)) oled_println("Score: " + str(score)) oled_show() if button_pressed(BTN_BACK): break time.sleep_ms(30) ir_stop() ``` ### Tips for IR Communication - Always call `ir_start()` first and `ir_stop()` when done - Poll `ir_read()` within 50 ms to avoid buffer overflow - A classic frame takes ~110 ms on wire; a 3-word frame takes ~230 ms - Use `ir_flush()` to clear stale frames before starting a new exchange - `ir_tx_power(10)` is useful for testing with two badges close together - IR is line-of-sight — badges need to be roughly pointed at each other --- ## 9. Firmware Updates & Community Apps The badge can update its own firmware over WiFi from GitHub Releases, and can fetch installable apps + user files (like the DOOM WAD) from a configurable Community Apps registry. Both systems are user-driven — the badge checks once a day in the background, but never installs anything without an explicit Confirm. ### Where state lives (mental model) Three storage tiers; one rule: **state in NVS, code on FATFS**. | Tier | Holds | Survives a... | |------|-------|---------------| | NVS | badge ID, WiFi creds, contacts, badgeInfo, `badge.kv` saves | every flash type | | FATFS | Python source, docs, images, doom1.wad, user uploads | firmware-only flash; **wiped** by `fatfs.bin` reflash + `--clear-extras` sync | | app0 | C++ binary + survival floor (`/lib`, `/matrixApps`) | replaced only by a firmware flash | Putting game saves in `badge.kv` (see § 4 Saving Data) means a firmware reflash, factory flash, or Community Apps install never loses them. Files on FATFS are re-pushable via JumperIDE / Community Apps / `python3 -m badge_sync sync`. See the [Storage Model](https://github.com/Architeuthis-Flux/Temporal-Replay-26-Badge/blob/main/firmware/docs/STORAGE-MODEL.md) for the full survival matrix. ### What the user sees - **Status bar glyph.** A small down-arrow appears immediately to the left of the WiFi icon when a newer firmware release has been cached. It disappears as soon as the install completes. - **"FW UPDATE" home tile.** Always visible. Label flips to **UPDATE** (with a notification badge dot) when an update is waiting; otherwise it reads as a "Check Updates" affordance. Confirm enters the Firmware Update screen. [Image: Home grid with FW UPDATE tile selected] - **"COMMUNITY APPS" home tile.** Always visible. Opens the Community Apps screen — a list of every entry in the remote registry with per-row status (`OK` for installed, `UPD` for update available, blank for not installed). The legacy "Asset Library" / "LIBRARY" label was renamed in v0.2 firmware. Empty when `community_apps_url` isn't configured (the default firmware ships with a working URL, so this only happens if you explicitly clear it). ### Update cadence The badge polls `api.github.com/repos///releases/latest` at most once every 24 hours, only when WiFi is connected. State (latest tag, asset URL, last-check timestamp) is persisted in NVS so the indicator survives reboots and offline use. Manual "Check now" from the Firmware Update screen ignores the cooldown. ### Installing a firmware update 1. Open **FW UPDATE** from the home grid. 2. Press Confirm to install (or to re-check, if you're already on the latest version). Battery must be ≥ 30 % unless USB is plugged in. 3. The screen shows a progress bar as the new image streams into the inactive OTA slot. Do not unplug. 4. The badge reboots into the new image. If it fails to boot, the bootloader rolls back automatically on the next reset — there is no way for an OTA to brick the badge as long as it has power. [Image: Firmware Update screen showing current/latest version and filesystem size] When a wider `ffat` partition is available but the volume header still reflects the old size, the same screen shows a filesystem line such as `FS: 5.9 MB ◇ 6.9 MB FS` — the diamond glyph is the expand affordance (see [Expanding storage](#expanding-storage-after-a-partition-bump) below). ### Community Apps The registry is a single JSON file the badge fetches once a day. Each entry has an `id`, `version`, download `url`, optional SHA-256, and a filesystem `dest_path` (or `dest_dir` for multi-file apps). The badge streams each file into a `.tmp`, verifies the hash if present, then atomically renames into place. The DOOM tile uses this transparently: if `/doom1.wad` is missing on the filesystem, **DOOM → Confirm** routes you to the Community Apps detail page for the WAD with a one-tap **Install** button. No need to sideload via `uploadfs`. ### Pushing files via JumperIDE JumperIDE (`ide.jumperless.org`) is the easiest non-WiFi path for getting files onto the badge: - **Save** (`Ctrl+S`) writes the open file to the badge over USB, same as `mpremote cp` would. - **Sync Filesystem** (planned button next to the firmware-update modal) does a one-click diff: it lists everything currently on the badge, compares against the upstream [`firmware/data/manifest.json`](https://github.com/Architeuthis-Flux/Temporal-Replay-26-Badge/blob/main/firmware/data/manifest.json), and pushes anything missing or stale. Useful after a firmware-only reflash where you want to refresh apps without losing your local edits. If you prefer a CLI: `python3 firmware/scripts/badge_sync.py sync /dev/cu.usbmodemXXXX` does the same diff from a shell. Disconnect any active serial monitor first (the badge port is single-owner). See `firmware/docs/STORAGE-MODEL.md` for full options. ### Re-flashing the FATFS partition Two ways to repopulate everything (apps, docs, images, the DOOM WAD) without touching firmware: ```bash cd firmware # Always use the bundled pio binary — the macOS / micromamba shell `pio` # shim often resolves to a Python without platformio installed. ~/.platformio/penv/bin/pio run -e echo -t uploadfs ``` This builds `fatfs.bin` from `firmware/data/` (mirror of `firmware/initial_filesystem/`) and writes it directly to the badge's ffat partition. Includes `/doom1.wad` (4 MB) which is otherwise downloaded over WiFi via Community Apps. NVS state (game saves, contacts, badge identity) is untouched. If you see `ModuleNotFoundError: No module named 'platformio'`, that's the system `pio` shim issue — call the bundled binary by absolute path as shown above. ### `settings.txt` keys ``` [ota] # Override the default GitHub Releases endpoint (advanced; usually # leave empty to use the build-baked default). manifest_url = # URL of the Community Apps registry JSON. Empty disables the # Community Apps tile. The legacy `asset_registry_url` key still # works for backwards compatibility. community_apps_url = https://raw.githubusercontent.com/Architeuthis-Flux/Temporal-Replay-26-Badge/main/registry/community_apps.json ``` ### `community_apps.json` schema (v2) Two entry kinds: single files and multi-file app bundles. ```json { "schema_version": 2, "assets": [ { "id": "doom1-shareware", "kind": "file", "name": "DOOM 1 Shareware WAD", "version": "1.9", "url": "https://...doom1.wad", "sha256": "", "size": 4196020, "dest_path": "/doom1.wad", "min_free_bytes": 4500000, "description": "..." }, { "id": "tardigotchi", "kind": "app", "name": "Tardigotchi", "version": "8b139b84ef0b", "dest_dir": "/apps/tardigotchi", "size": 33744, "description": "Hatch and care for a tiny tardigrade.", "files": [ {"path": "/main.py", "size": 187, "sha256": "...", "url": "https://..."}, {"path": "/engine.py", "size": 24006, "sha256": "...", "url": "https://..."}, {"path": "/icon.py", "size": 9551, "sha256": "...", "url": "https://..."} ] } ] } ``` App bundle file lists are inlined directly into the registry — there's no per-app `manifest.json`. `registry/community_apps.json` is auto-generated by `firmware/scripts/generate_startup_files.py` from `firmware/initial_filesystem/`; drop your app folder there and re-run the script (or trigger a PlatformIO build). The legacy `registry/registry.json` (schema v1) is frozen for backwards compatibility; new entries go into v2. See `firmware/docs/OTA-MAINTAINER.md` in the firmware repo for the full maintainer walkthrough, including how to host the registry on Cloudflare R2 / Pages. ### Expanding storage after a partition bump Sometimes a firmware update ships with a wider `ffat` partition (the 2026 v0.1.5 bump grew the FAT partition from 6 MB to ~7.9 MB to reuse unused flash). The new firmware will boot fine on existing badges and keep all your data — but the FAT volume header is still sized for the old partition, so you only see the old capacity until a reformat writes a new header. When this happens, the **Firmware Update** screen shows a filesystem line with the current size and an expand affordance (e.g. `FS: 5.9 MB ◇ 6.9 MB FS`). Select the diamond glyph on that line to start the reformat flow: 1. **Preflight** — battery/USB, partition layout, and recovery blob checks must all pass before Continue is offered. [Image: Expand preflight: migrate ffat to the new size] 2. First confirm: shows what gets wiped (contacts, nametags, WAD, `settings.txt`). 3. **Final confirmation** — warns that the partition table is rewritten, `ffat` is wiped, and the badge auto-reboots. [Image: Final confirmation before partition expand] 4. The badge formats `ffat` and reboots into a clean filesystem with the full partition size available. A **recovery QR** on the expand path documents the USB + `esptool write_flash` fallback if anything goes wrong mid-migration: [Image: Recovery QR for USB esptool write_flash] The option only appears when there's a real gap to recover (≥ 256 KB above what FAT metadata explains away). On freshly USB-flashed badges, the FAT is sized to the partition at first boot and you'll never see this prompt. ### Forking the firmware To point OTA at a different repo (or look for a different asset filename in the release), edit `firmware/platformio.ini`: ```ini '-DOTA_GITHUB_REPO="YourOrg/YourFork"' '-DOTA_ASSET_NAME="firmware-yourfork.bin"' ``` The badge will look for an asset of exactly that name on the latest release of the configured repo. ### Security stance This is open-source firmware. There is no image signing, no certificate pinning, no PIN, no auth. SHA-256 hashes are corruption checks, not signatures. The threat model is "don't brick the badge", which is mitigated by: - battery ≥ 30 % guard (unless USB is plugged in), - bootloader auto-rollback if the new image fails to boot, - atomic rename for asset files. If your fleet needs a hardened OTA, fork `firmware/src/ota/` and bring your own keys. --- ## 10. Tips and Gotchas ### Memory Management - **128 KB heap.** Call `gc.collect()` regularly, especially in game loops. - Avoid creating large temporary objects. Reuse buffers when possible. - Check available memory: `gc.mem_free()` returns bytes free. - Source files have a ~16 KB limit per file due to the MicroPython compiler. Split large apps into multiple modules. ### Display - `oled_show()` is required after any drawing operation. Nothing appears on screen until you call it. - `oled_clear()` resets the cursor to (0,0). Pass `True` to also refresh: `oled_clear(True)`. - The display is 128×64 — plan your layouts accordingly. The usable area with `badge_ui` chrome is roughly Y: 10–52 (between header and footer). ### LED Matrix - Use `led_override_begin()` / `led_override_end()` when drawing directly on the matrix. Without it, the ambient LED mode may overwrite your pixels. - `led_clear()` at the end of your app to be a good citizen. - Matrix app callbacks (`matrix_app_start`) run from the service pump, not your main loop — keep them fast (no blocking, no heavy computation). ### IR - **IR is mode-gated** — only works after `ir_start()`. Other screens (Boop) share the hardware. - The RX buffer is 8 frames deep. If you don't read within ~50 ms per frame, frames get dropped. - Always `ir_stop()` and `ir_flush()` in your cleanup code. ### Input - `button_pressed()` consumes the event — calling it twice for the same button in the same loop iteration will miss the second call. Read it once and store the result. - The joystick center varies per unit (~2048 typical). Always use a dead zone of at least 200–300. ### General - **f-strings are available**, but string concatenation also works. - **No `import badge` needed** in the entry script — all functions are auto-injected. In imported modules, use `from badge import *`. - **Escape chord:** Hold all 4 face buttons for ~1 second to force-exit any stuck app. - **Clean up** before exiting: `led_clear()`, `haptic_off()`, `no_tone()`, `ir_stop()`, `mouse_overlay(False)`. ### Updates & WiFi - **WiFi is required for OTA and Community Apps.** Configure it once via Settings → WiFi Setup. The badge auto-connects on boot. JumperIDE works over USB so it can push files without WiFi. - **Don't unplug during a firmware install.** A brownout mid-flash is the only thing that can leave the badge in a bad state — and even then the bootloader will roll back on the next reset. - **Asset downloads can take several minutes** on slow conference WiFi (the DOOM WAD is 4 MB). The progress screen shows live KB counts; if it stalls for more than 30 s, cancel and retry. --- ## Quick Reference Card ``` ┌──────────────────────────────────────────────────┐ │ OLED (128×64) │ │ oled_clear() → oled_print() → oled_show() │ │ │ │ LED Matrix (8×8) │ │ led_show_image(IMG_HEART) │ │ led_set_frame([row0..row7], brightness) │ │ │ │ Buttons │ │ button_pressed(BTN_CONFIRM) → True once │ │ button(BTN_UP) → True while held │ │ │ │ Joystick │ │ joy_x() → 0–4095 joy_y() → 0–4095 │ │ │ │ Haptics │ │ haptic_pulse() tone(440, 200) │ │ │ │ IR │ │ ir_start() → ir_send(a,c) → ir_read() → tuple │ │ │ │ IMU │ │ imu_tilt_x() imu_motion() imu_face_down() │ │ │ │ Mouse │ │ mouse_overlay(True) → mouse_clicked() → btn_id│ │ │ │ Files │ │ open("/apps/x/save.json","w").write(data) │ │ │ │ Exit │ │ exit() or hold all 4 buttons │ └──────────────────────────────────────────────────┘ ``` --- # API Reference All functions and constants from the `badge` module are automatically imported into the global namespace. You can call them directly (e.g. `button(BTN_CONFIRM)`) without needing the `badge.` prefix. --- [Init](#init): * `init()` — Initialize the badge hardware bridge (called automatically) [OLED Display](#oled-display): * `oled_print(text)` — Print text at cursor * `oled_println(text)` — Print text + newline + show * `oled_clear([show])` — Clear display * `oled_show()` — Refresh display * `oled_set_cursor(x, y)` — Set text cursor * `oled_set_text_size(size)` — Set text size (1–4) * `oled_get_text_size()` — Get text size * `oled_invert(enable)` — Invert display colors * `oled_text_width(text)` — Get pixel width of a string * `oled_text_height()` — Get pixel height of current font * `oled_set_font(name)` — Set font by name * `oled_get_fonts()` — Get available font names * `oled_get_current_font()` — Get current font name * `oled_set_pixel(x, y, color)` — Set single pixel * `oled_get_pixel(x, y)` — Read single pixel * `oled_draw_box(x, y, w, h)` — Draw filled rectangle * `oled_set_draw_color(color)` — Set draw color (0=black, 1=white, 2=XOR) * `oled_get_framebuffer()` — Get framebuffer as bytes * `oled_set_framebuffer(data)` — Set framebuffer from bytes * `oled_get_framebuffer_size()` — Get (width, height, bytes) * `oled_screenshot([mode])` — Dump OLED framebuffer to terminal as block characters [Native UI Chrome](#native-ui-chrome): * `ui_header(title, [right])` — Draw the standard header and rule * `ui_action_bar([left_button], [left_label], [right_button], [right_label])` — Draw footer actions with native button glyphs * `ui_chrome(title, [right], [left_button], [left_label], [right_button], [right_label])` — Clear and draw standard header/footer chrome * `ui_inline_hint(x, y, hint)` — Draw an inline hint with native button glyphs * `ui_inline_hint_right(right_x, y, hint)` — Right-align an inline hint * `ui_measure_hint(hint)` — Return an inline hint's pixel width [Mouse Overlay](#mouse-overlay): * `mouse_overlay(enable)` — Enable/disable cursor overlay * `mouse_set_bitmap(data, w, h)` — Set cursor sprite (row-major bitmap, max 32×32) * `mouse_x()` — Current cursor X position * `mouse_y()` — Current cursor Y position * `mouse_set_pos(x, y)` — Warp cursor to position * `mouse_clicked()` — Read last click button ID (-1 if none) * `mouse_set_speed(speed)` — Set cursor speed (1–20, default 3) * `mouse_set_mode(mode)` — Set positioning mode (`MOUSE_ABSOLUTE` or `MOUSE_RELATIVE`, default `MOUSE_RELATIVE`) [Buttons & Joystick](#buttons-joystick): * `button(id)` — Read button state (True if held) * `button_pressed(id)` — Edge-triggered press (True once per press) * `button_held_ms(id)` — Milliseconds button has been held * `joy_x()` — Joystick X axis (0–4095) * `joy_y()` — Joystick Y axis (0–4095) [LED Matrix](#led-matrix-88) (8×8): * `led_brightness(value)` — Set global brightness (0–255) * `led_clear()` — Turn off all LEDs * `led_fill([brightness])` — Turn on all LEDs * `led_set_pixel(x, y, brightness)` — Set single LED * `led_get_pixel(x, y)` — Read single LED brightness * `led_show_image(name)` — Show builtin image by name * `led_set_frame(rows, [brightness])` — Draw 8×8 bitmask pattern * `led_start_animation(name, [interval_ms])` — Start builtin animation * `led_stop_animation()` — Stop current animation * `led_override_begin()` — Pause ambient LED mode for foreground drawing * `led_override_end()` — Release foreground drawing and restore ambient LEDs * `led_screenshot([mode], [ansi])` — Dump LED matrix to terminal as block characters * `screenshot([mode], [ansi])` — Dump OLED and LED matrix together to terminal; REPL hotkey: bare `o` + Enter [Matrix App Host](#matrix-app-host) (Background LED Callbacks): * `matrix_app_start(callback, [interval_ms], [brightness])` — Register a Python callback for background LED matrix rendering * `matrix_app_set_speed(interval_ms)` — Change the tick interval * `matrix_app_set_brightness(brightness)` — Change the LED brightness * `matrix_app_stop()` — Unregister the callback * `matrix_app_active()` — Check if a callback is registered * `matrix_app_info()` — Return diagnostic tuple [App Manifest](#app-manifest) (Folder-app metadata): * `__title__`, `__description__`, `__icon__`, `__matrix_title__`, `__order__` — Top-level dunders read from `/apps//main.py` to decorate the main menu tile * `icon.py` — 12×12 packed-XBM `DATA` tuple for the home-screen icon * `matrix.py` — Persistent LED-matrix script registered via `matrix_app_start` * `rescan_apps()` — Hot-refresh the registry after editing manifests [IMU](#imu): * `imu_ready()` — Check if IMU is initialized * `imu_tilt_x()` — X-axis tilt in milli-g * `imu_tilt_y()` — Y-axis tilt in milli-g * `imu_accel_z()` — Z-axis acceleration in milli-g * `imu_face_down()` — True if badge is face-down * `imu_motion()` — Consume motion event (True if motion detected since last call) [Haptics](#haptics) (Vibration Motor + Coil Tone): * `haptic_pulse([strength], [duration_ms], [freq_hz])` — Fire vibration pulse * `haptic_strength([value])` — Get or set motor strength (0–255) * `haptic_off()` — Stop motor * `tone(freq_hz, [duration_ms], [duty])` — Play audible tone from motor coil * `no_tone()` — Stop tone * `tone_playing()` — Check if tone is active [IR Send/Receive](#ir-sendreceive) (NEC Protocol): * `ir_send(addr, cmd)` — Transmit one 1-byte-addr / 1-byte-cmd NEC frame * `ir_start()` — Start IR receive mode * `ir_stop()` — Stop IR receive, flush queue * `ir_available()` — Check if a received frame is waiting * `ir_read()` — Read received (addr, cmd) as a tuple * `ir_send_words(words)` — Transmit a multi-word NEC frame (1–64 × 32-bit) * `ir_read_words()` — Read a received multi-word NEC frame as a tuple * `ir_flush()` — Drop every pending RX frame * `ir_tx_power([percent])` — Get/set IR carrier duty (1–50%) [Badge Identity & Boops](#badge-identity-boops): * `my_uuid()` — Return this badge's 12-char hex UID * `boops()` — Return `/boops.json` contents as a string [Script Control](#script-control): * `exit()` — Raise `SystemExit` to cleanly stop the running app * `dev(*args)` — Test harness dispatcher *(dev builds only)* [Filesystem Access](#filesystem-access): * Standard `os` module — `listdir`, `mkdir`, `remove`, `rename`, `stat`, etc. * Standard `open()` / `read()` / `write()` / `close()` --- ## Init ### `init()` Initialize the badge hardware bridge. This is called automatically when the badge module loads — you should not normally need to call it. Returns `0` on success; raises `OSError` on failure. --- ## OLED Display 128×64 monochrome SSD1306 OLED display controlled via U8G2. ### `oled_print(text)` Print text at the current cursor position. Does not refresh the display automatically — call `oled_show()` to make it visible. * `text`: String to display. ### `oled_println(text)` Print text followed by a newline, then automatically refresh the display. * `text`: String to display. **Example:** ```jython oled_clear() oled_println("Hello, badge!") oled_println("Line 2") ``` ### `oled_clear([show])` Clear the display and reset cursor to (0, 0). * `show` (optional): If `True`, refresh display immediately. Default `False`. **Example:** ```jython oled_clear() # Clear buffer only oled_clear(True) # Clear and refresh immediately ``` ### `oled_show()` Refresh the display to show buffered changes. Required after `oled_print()`, `oled_set_pixel()`, or `oled_set_framebuffer()` to make changes visible. ### `oled_set_cursor(x, y)` Move the text cursor to pixel coordinates. * `x`: X position (0–127). * `y`: Y position (0–63). ### `oled_set_text_size(size)` Set the text rendering size. * `size`: Text size multiplier (1–4). Returns `True` on success. ### `oled_get_text_size()` Returns the current text size (1–4). ### `oled_invert(enable)` Invert the display colors. * `enable`: `True` to invert, `False` for normal. ### `oled_text_width(text)` Get the pixel width of a string in the current font. * `text`: The string to measure. * Returns the width in pixels. **Example:** ```jython w = oled_text_width("Hello") x = (128 - w) // 2 oled_set_cursor(x, 30) oled_print("Hello") oled_show() ``` ### `oled_text_height()` Get the maximum character height of the current font. * Returns the height in pixels. ### `oled_set_font(name)` Set the font by name. * `name`: Font family name (case-sensitive). * Returns `True` if font was found, `False` otherwise. ### `oled_get_fonts()` Returns a comma-separated string of available font names. **Example:** ```jython fonts = oled_get_fonts().split(",") for f in fonts: oled_set_font(f) oled_clear() oled_println(f) import time; time.sleep(1) ``` ### `oled_get_current_font()` Returns the name of the currently active font. ### `oled_set_pixel(x, y, color)` Set a single pixel in the framebuffer. * `x`: X coordinate (0–127). * `y`: Y coordinate (0–63). * `color`: `1` for white/on, `0` for black/off. Call `oled_show()` after setting pixels to make changes visible. ### `oled_get_pixel(x, y)` Read the color of a single pixel. * Returns `1` (white/on) or `0` (black/off). ### `oled_draw_box(x, y, w, h)` Draw a filled rectangle in the framebuffer using the current draw color. * `x`: Left edge (0–127). * `y`: Top edge (0–63). * `w`: Width in pixels. * `h`: Height in pixels. Call `oled_show()` after drawing to make changes visible. **Example:** ```jython oled_clear() oled_draw_box(10, 10, 50, 20) oled_set_draw_color(0) oled_set_cursor(12, 12) oled_print("Hello") oled_set_draw_color(1) oled_show() ``` ### `oled_set_draw_color(color)` Set the drawing color for subsequent draw operations. * `color`: `0` = black/off, `1` = white/on (default), `2` = XOR (inverts existing pixels). ### `oled_get_framebuffer()` Returns the entire display framebuffer as a `bytes` object. Format: 1 bit per pixel, organized in vertical bytes (Adafruit SSD1306 format). ### `oled_set_framebuffer(data)` Replace the entire display framebuffer and refresh. * `data`: `bytes` or `bytearray` of the correct size (typically 1024 bytes for 128×64). * Returns `True` on success, `False` on size mismatch. ### `oled_get_framebuffer_size()` Returns a tuple `(width, height, buffer_size_bytes)`. **Example:** ```jython w, h, size = oled_get_framebuffer_size() print(str(w) + "x" + str(h) + ", " + str(size) + " bytes") ``` ### `oled_screenshot([mode])` Dump the current OLED framebuffer to the serial console as a bordered block-character image. Useful for inspecting display state without a camera, and for capturing UI reference while writing docs (see also `screenshot()` and the REPL `o` hotkey below). * `mode` (optional): Rendering size on the following ladder (smallest first): * `0.5` (float) — **quarter-block**, 2×2 pixels per glyph. OLED 64×32 chars. Smallest output. * `0` (int) — **half-block**, 1×2 pixels per glyph (`▀`/`▄`/`█`/space). OLED 128×32 chars. 1W × 1W square cells on 2:1 monospace fonts. * `N` (int, ≥1) — **square** at `2N` chars wide × `N` char-rows tall per pixel (2NW × 2NW square cells). `1` is the smallest square (2×1 chars), `2` is twice as big (4×2 chars), etc. * `N.5` (float, ≥1.5) — **tall** escape hatch: each pixel = `round(2(N−1))` chars wide × 1 char-row tall. `1.5` gives 1W × 2W per pixel (literal 1-char-per-pixel), `2.5` gives 3W × 2W, etc. Use when you want char-per-pixel output without aspect correction. Default: `1` (smallest square: 2 chars × 1 row per pixel). **Example:** ```jython oled_clear() oled_println("Hello!") oled_screenshot() # int 1 — smallest square, 256 chars wide oled_screenshot(0.5) # float — quarter-block compact, 64 chars wide oled_screenshot(0) # int 0 — half-block compact, 128 chars wide oled_screenshot(2) # int 2 — bigger square, 512 chars wide oled_screenshot(1.5) # float — literal 1 char/pixel, 128 chars wide tall ``` --- ## Native UI Chrome These helpers draw the same header, footer, and button glyph style used by the firmware screens. App code should usually import `badge_ui`, which wraps these native functions with Python conveniences like `ui.chrome(...)`. `badge_ui` also includes `ui.hint(...)`, `ui.hint_text(...)`, and `ui.hint_row(...)` helpers for composing multiple glyph-backed action hints without hand-building strings throughout an app. ### `ui_header(title, [right])` Draw the standard small header and divider. `right` is optional top-right text. ### `ui_action_bar([left_button], [left_label], [right_button], [right_label])` Draw footer actions with native button glyphs. Button names include `OK`, `BACK`, `X`, `Y`, `A`, and `B`; semantic names respect the badge's confirm/back swap setting. ### `ui_chrome(title, [right], [left_button], [left_label], [right_button], [right_label])` Clear the OLED buffer, draw the standard header, then draw the footer action bar. Call `oled_show()` after drawing your screen content. ### `ui_inline_hint(x, y, hint)` Draw inline text with native button glyph replacement, such as `"OK:start"` or `"BACK quit"`. Returns the drawn width in pixels. ### `ui_inline_hint_right(right_x, y, hint)` Right-align an inline hint to `right_x`. Returns the drawn width in pixels. ### `ui_measure_hint(hint)` Return the pixel width an inline hint will use. --- ## Buttons & Joystick Four face buttons and a 2-axis analog joystick. The directional constants are kept for compatibility; new apps should prefer the semantic aliases where they fit the interaction. ### Button Constants ```jython BTN_RIGHT = 0 BTN_DOWN = 1 BTN_LEFT = 2 BTN_UP = 3 BTN_CIRCLE = BTN_RIGHT BTN_CROSS = BTN_DOWN BTN_SQUARE = BTN_LEFT BTN_TRIANGLE = BTN_UP BTN_CONFIRM = 4 BTN_SAVE = BTN_CONFIRM BTN_BACK = 5 BTN_PRESETS = BTN_TRIANGLE ``` `BTN_CONFIRM`/`BTN_SAVE` and `BTN_BACK` follow the firmware `swap_ok` setting: the default is B/Circle confirm and A/Cross back; setting `swap_ok = 0` uses A/Cross confirm and B/Circle back. The physical constants and PlayStation-style shape constants always refer to the actual hardware button. ### `button(id)` Read the current state of a button. * `id`: Button constant (`BTN_CONFIRM`, `BTN_BACK`, `BTN_PRESETS`, etc.). * Returns `True` if the button is currently held down. **Example:** ```jython if button(BTN_CONFIRM): oled_println("confirm held!") ``` ### `button_pressed(id)` Edge-triggered button press detection. Returns `True` **once** per physical press, then `False` until the button is released and pressed again. Consumes the press event on read. * `id`: Button constant. * Returns `True` if a new press was detected since the last call. **Example:** ```jython import time count = 0 while True: if button_pressed(BTN_CONFIRM): count += 1 oled_clear() oled_println("Presses: " + str(count)) time.sleep(0.02) ``` ### `button_held_ms(id)` Get how long a button has been continuously held. * `id`: Button constant. * Returns milliseconds the button has been held, or `0` if not pressed. **Example:** ```jython ms = button_held_ms(BTN_DOWN) if ms > 1000: oled_println("Long press!") ``` ### `joy_x()` Read the joystick X axis. * Returns an integer 0–4095. Center is approximately 2047. ### `joy_y()` Read the joystick Y axis. * Returns an integer 0–4095. Center is approximately 2047. **Example:** ```jython x = joy_x() y = joy_y() oled_clear() oled_println("X:" + str(x) + " Y:" + str(y)) ``` --- ## LED Matrix (8×8) 8×8 LED matrix driven by the IS31FL3731 with per-pixel PWM brightness control. ### Image Constants Builtin image names for use with `led_show_image()`: ```jython IMG_SMILEY = "smiley" IMG_HEART = "heart" IMG_ARROW_UP = "arrow_up" IMG_ARROW_DOWN = "arrow_down" IMG_X_MARK = "x_mark" IMG_DOT = "dot" ``` ### Animation Constants Builtin animation names for use with `led_start_animation()`: ```jython ANIM_SPINNER = "spinner" ANIM_BLINK_SMILEY = "blink_smiley" ANIM_PULSE_HEART = "pulse_heart" ``` ### `led_brightness(value)` Set the global LED brightness. * `value`: Brightness level (0–255). `0` is off, `255` is maximum. ### `led_clear()` Turn off all LEDs on the matrix. ### `led_fill([brightness])` Turn on all LEDs. If `brightness` is omitted, uses the current global brightness. * `brightness` (optional): Per-pixel brightness (0–255). ### `led_set_pixel(x, y, brightness)` Set a single LED brightness. * `x`: Column (0–7). * `y`: Row (0–7). * `brightness`: LED brightness (0–255). **Example:** ```jython led_clear() led_set_pixel(3, 3, 100) led_set_pixel(4, 4, 100) ``` ### `led_get_pixel(x, y)` Read the brightness of a single LED. * Returns brightness value (0–255). ### `led_show_image(name)` Display a builtin image on the matrix. * `name`: Image name string (see Image Constants above). * Returns `True` if the image was found. **Example:** ```jython led_show_image(IMG_HEART) import time time.sleep(2) led_show_image("smiley") # string name works too ``` ### `led_set_frame(rows, [brightness])` Draw an arbitrary 8×8 pattern from a list of row bitmasks. Each row is a uint8 where the MSB is the leftmost pixel. * `rows`: List or tuple of exactly 8 integers (0–255), one per row. * `brightness` (optional): On-pixel brightness (0–255). Defaults to current global brightness. **Example:** ```jython # Draw a smiley face led_set_frame([ 0b00111100, 0b01000010, 0b10100101, 0b10000001, 0b10100101, 0b10011001, 0b01000010, 0b00111100, ]) # Draw an X with explicit brightness led_set_frame([ 0b10000001, 0b01000010, 0b00100100, 0b00011000, 0b00011000, 0b00100100, 0b01000010, 0b10000001, ], 50) # dim ``` ### `led_start_animation(name, [interval_ms])` Start a builtin animation on the matrix. * `name`: Animation name string (see Animation Constants above). * `interval_ms` (optional): Frame interval in milliseconds. Default 120ms. * Returns `True` on success. **Example:** ```jython led_start_animation(ANIM_PULSE_HEART) import time time.sleep(5) led_stop_animation() ``` ### `led_stop_animation()` Stop the currently running animation. ### `led_override_begin()` Pause the saved ambient LED mode so a foreground app can draw on the matrix. Call this before direct LED drawing when you want to temporarily override the LED app. ### `led_override_end()` Release a foreground LED override and restore the saved ambient LED mode. Call this from cleanup paths after `led_override_begin()`. ### `led_screenshot([mode], [ansi])` Dump the current 8×8 LED matrix to the serial console as a bordered block-character image. * `mode` (optional): Rendering size on the same ladder as `oled_screenshot()`: * `0.5` (float) — **quarter-block**, 4×4 chars (6 wide with border). * `0` (int) — **half-block**, 8×4 chars (10 wide with border). * `N` (int, ≥1) — **square** `2N` chars × `N` char-rows per LED. * `N.5` (float, ≥1.5) — **tall** escape hatch, `round(2(N−1))` chars × 1 char-row per LED. Default: `1` (smallest square: 2 chars × 1 row per LED, 16 chars wide). * `ansi` (optional): If `True`, lit LEDs print in bright red via ANSI escape codes. Requires a terminal that interprets ANSI (not PlatformIO device monitor). Default: `True`. **Example:** ```jython led_set_pixel(0, 0, 255) led_set_pixel(7, 7, 255) led_screenshot() # int 1 — 2W × 2W square per LED led_screenshot(0.5) # float — quarter-block compact (4×4 chars) led_screenshot(0) # int 0 — half-block compact (8×4 chars) led_screenshot(2, False) # int 2 — bigger square, no ANSI colour led_screenshot(1.5) # float — 1 char × 1 char-row per LED (tall) ``` ### `screenshot([mode], [ansi])` Dump both the OLED and the LED matrix to the serial console in one call, with the LED matrix centred horizontally below the OLED. The quickest way to snapshot the full badge display state. * `mode` (optional): Rendering size for the OLED, on the same ladder as `oled_screenshot()`. The LED renders **one size larger** (so each LED visual cell is a step bigger than an OLED pixel), staying in the same int/float family as the OLED: * `0.5` (float) — OLED quarter-block (66 wide). LED at float 1.5 (1 char × 1 row per LED, tall, 10 wide). * `0` (int) — OLED half-block (130 wide). LED at int 1 (square 2W × 2W per LED, 18 wide). * `N` (int, ≥1) — OLED pixel = 2N × N chars (square). LED cell = 2(N+1) × (N+1) chars (square, one step larger). * `N.5` (float, ≥1.5) — OLED tall. LED also tall, one step larger. Default: `0`. * `ansi` (optional): ANSI bright-red for lit LED pixels. Default: `True`. **Example:** ```jython screenshot() # default — half-block OLED + small square LED screenshot(0.5) # quarter-block OLED + tiny tall LED screenshot(1) # OLED 2W square, LED 4W square screenshot(2) # OLED 4W square, LED 6W square screenshot(1.5) # tall OLED (1 char/pixel) + tall LED (3 chars/LED) screenshot(0, False) # default, no ANSI colour ``` **REPL hotkey:** typing a bare `o` followed by `` at the MicroPython REPL runs `screenshot()` with default arguments. `o` is a global singleton whose `repr` triggers the screenshot, so `print(o)` and `repr(o)` work the same way. Rebinding (`o = 42`) replaces the hotkey within the running session. **Docs workflow:** open the screen on the badge, connect over USB serial (JumperIDE, `mpremote`, or `python3 serial_log.py`), type `o` at the REPL, and copy or screenshot the terminal output. Default `screenshot()` uses half-block OLED glyphs (readable on 2:1 monospace fonts) with the LED matrix centred below. Use `screenshot(0, False)` if your terminal does not interpret ANSI colour. The [Developer Guide](badge-developer-guide.md#serial-screenshots-oled--led) has mode-ladder examples and terminal tips. --- ## Matrix App Host Register a Python callback that the firmware calls periodically to render on the 8×8 LED matrix in the background. This lets apps run LED animations that continue while the main Python loop handles OLED and input, or even while no Python code is running (the callback fires from the service pump). ### `matrix_app_start(callback, [interval_ms], [brightness])` Register a callable to be invoked every `interval_ms` milliseconds. * `callback`: A callable that accepts one argument — the current `millis()` timestamp. Pass `None` to clear the callback. * `interval_ms` (optional): Tick interval in milliseconds. Minimum 16 ms. Defaults to the firmware's ambient interval. * `brightness` (optional): LED brightness (0–255). Defaults to the firmware's ambient brightness. **Example:** ```jython frame = 0 def led_tick(now_ms): global frame led_clear() col = frame % 8 for row in range(8): led_set_pixel(col, row, 80) frame += 1 matrix_app_start(led_tick, 100, 40) ``` ### `matrix_app_set_speed(interval_ms)` Change the tick interval for the active callback. * `interval_ms`: New interval in milliseconds (minimum 16 ms). * Returns the clamped interval. ### `matrix_app_set_brightness(brightness)` Change the LED brightness for the active callback. * `brightness`: New brightness (0–255). * Returns the clamped brightness. ### `matrix_app_stop()` Unregister the callback and restore default ambient LED mode. ### `matrix_app_active()` Check if a background callback is currently registered. * Returns `True` if a callback is active. ### `matrix_app_info()` Return a diagnostic tuple with the current matrix app state. * Returns `(active, saved, interval_ms, brightness, overridden, invocations)`. --- ## App Manifest The firmware's `AppRegistry` discovers folder apps under `/apps//` at boot (and on demand via `rescan_apps()`) and exposes them on the main grid menu. Each app's tile is decorated from a small set of optional top-level dunder assignments inside `main.py`. The scanner reads the first ~2 KB of the file as text and parses the assignments without executing any code, so your dunders must sit at the top of the file with simple string literals. ### Dunders Place these at the top of `/apps//main.py`: | Dunder | Type | Default | Purpose | |--------|------|---------|---------| | `__title__` | `str` (≤ 19 chars) | slug, title-cased | Main-menu tile label | | `__description__` | `str` (≤ 63 chars) | empty | Detail panel text | | `__icon__` | `str` (path or inline tuple) | tries `icon.py` | 12×12 home-screen icon | | `__matrix_title__` | `str` (≤ 19 chars) | `__title__` | MATRIX APPS picker label (only when `matrix.py` is present) | | `__order__` | `int` (signed) | `10000 + discovery index` | Sort key on the main grid; lower = earlier | ```jython """My Game — Tamagotchi-style desk pet.""" __title__ = "My Game" __description__ = "A tiny pet that lives in your pocket." __icon__ = "icon.py" __matrix_title__ = "Pet" # ... rest of main.py ... ``` The slug is the folder name. It must match `[A-Za-z0-9_-]+` and may not start with a `.`; anything else is silently skipped by the scanner. The registry holds at most 32 dynamic apps per badge. ### `icon.py` — Home-Screen Icon Path resolution for `__icon__`: * Bare filename (`"icon.py"`) → `/apps//icon.py` * Absolute path (`"/apps/foo/icon.py"`) → used as-is * Inline tuple (`"(0xFF, 0x..., )"`) → parsed directly The icon file just needs a top-level `DATA = (...)` tuple of 24 bytes arranged as a 12×12 packed XBM (2 bytes per row × 12 rows). Bit 0 of each byte is the leftmost pixel — same byte order as U8G2's `drawXBM`. The high 4 bits of every odd byte are unused (the row is only 12 wide). `WIDTH` / `HEIGHT` are decorative; the firmware always reads 12×12. ```jython """My Game icon.""" WIDTH = 12 HEIGHT = 12 # Two bytes per row (low = cols 0..7, high = cols 8..11). Binary # literals so the dot pattern is visible in the source. XBM is # LSB-first, so reading the literal left-to-right gives the mirrored # row — that's accepted, the bit values are still correct. DATA = ( 0b01110111, 0b00000111, 0b01110111, 0b00000111, 0b00000000, 0b00000000, 0b01100000, 0b00000000, 0b01100000, 0b00000000, 0b11111100, 0b00000011, 0b00000000, 0b00000000, 0b00111110, 0b00000000, 0b00100000, 0b00000000, 0b11100000, 0b00000011, 0b00000000, 0b00000010, 0b11000000, 0b00000011, ) ``` If `__icon__` is omitted, the registry still tries `/apps//icon.py` opportunistically. If that file is missing or unparsable the tile falls back to the generic apps glyph. ### `matrix.py` — Persistent Matrix App A sibling `matrix.py` next to `main.py` enables the app's slot in the **MATRIX APPS** picker (main menu → MATRIX APPS). Selecting it persists the slug to `/led_state.json` and re-sources `matrix.py` once on every boot that selection survives. The script's only job is to register a callback via [`matrix_app_start`](#matrix-app-host) and return — *do not* spin a main loop. ```jython """Drifting-dot ambient.""" __matrix_title__ = "Drift" import badge _phase = 0 def _tick(now_ms): global _phase _phase = (_phase + 1) & 7 frame = [0] * 8 frame[7] = 0x80 >> _phase badge.led_set_frame(frame) badge.matrix_app_start(_tick, 250, 24) ``` Same constraints as any other `matrix_app_start` callback: it runs from the firmware service pump, so keep ticks fast and don't block. The firmware tears down the previous callback for you when the user picks a different matrix app or any built-in mode (Sparkle, Off, etc.) — there's no need to call `matrix_app_stop()` from your script when switching modes. ### `rescan_apps()` Force the registry to re-scan `/apps/` after editing manifests, without rebooting: ```jython import badge badge.rescan_apps() ``` This rebuilds the main-menu grid in place. Existing screens stay open. ### Tile Order (`__order__`) The main grid is rendered in stable-sort order by a signed `int16` key. Three layers feed the key, each overriding the previous one: 1. **Defaults.** Curated tiles use `10 × array index` (with `SETTINGS` pinned to `30000` so it stays at the back). Dynamic apps default to `10000 + discovery index`. 2. **App manifest.** `__order__ = 50` at the top of `main.py` claims a specific slot. Any signed integer literal works. 3. **User override.** The manual reorder UI (Settings → Menu → Reorder) writes per-label overrides into the `menu_order` NVS namespace; those override both of the above. Ties resolve by insertion order — duplicate keys keep the order established by the previous layer. Negative keys land before all curated tiles; large keys land near `SETTINGS`. ```jython __order__ = -10 # before BOOP / CONTACTS / … __order__ = 25 # between MAP (30) and SCHEDULE (40) __order__ = 9999 # nearly last ``` ### Manual Reorder Screen Players can rearrange the grid themselves at runtime: > Settings → **Menu → Reorder** | Button | Action | |--------|--------| | Joystick Y | Move cursor up/down | | `X` | Pick up / drop a row | | `A` (confirm) | Save and rebuild | | `B` (back) | Cancel without saving | While picked up, dragging Y swaps the row through the list in real time. Saving writes the new positions into NVS (`menu_order` namespace, FNV-1a-hashed labels as keys). **Settings → Menu → Reset Order** wipes the namespace and returns every tile to its default order. --- ## IMU (Accelerometer) LIS2DH12 3-axis accelerometer for tilt sensing and motion detection. ### `imu_ready()` Check if the IMU is initialized and taking readings. * Returns `True` if the IMU is ready. ### `imu_tilt_x()` Read the smoothed X-axis tilt. * Returns a float in milli-g (mG). Typical range ±1000 mG. ### `imu_tilt_y()` Read the smoothed Y-axis tilt. * Returns a float in milli-g (mG). Typical range ±1000 mG. ### `imu_accel_z()` Read the Z-axis acceleration. * Returns a float in milli-g (mG). ~1000 mG when stationary (1g gravity). ### `imu_face_down()` Check if the badge is face-down. * Returns `True` if the badge is face-down (Z-axis below threshold). ### `imu_motion()` Check for a motion event. **Consumes** the event on read — calling again returns `False` until new motion is detected. * Returns `True` if motion was detected since the last call. **Example:** ```jython import time while True: if imu_motion(): oled_clear() oled_println("Motion!") haptic_pulse() x = imu_tilt_x() y = imu_tilt_y() # Map tilt to LED matrix pixel px = int((x + 1000) / 250) py = int((y + 1000) / 250) px = max(0, min(7, px)) py = max(0, min(7, py)) led_clear() led_set_pixel(px, py, 100) time.sleep(0.05) ``` --- ## Haptics Vibration motor with PWM control and audible coil tone support. The motor can produce haptic feedback pulses or, at very low duty cycles, audible tones from coil vibration. ### `haptic_pulse([strength], [duration_ms], [freq_hz])` Fire a haptic vibration pulse. All parameters are optional — omitted values use the configured defaults (strength ~155, duration ~35ms, frequency ~80Hz). * `strength` (optional): Motor intensity (0–255). * `duration_ms` (optional): Pulse duration in milliseconds. * `freq_hz` (optional): PWM carrier frequency in Hz. **Example:** ```jython haptic_pulse() # Default pulse haptic_pulse(200) # Stronger haptic_pulse(100, 50) # Medium strength, 50ms haptic_pulse(255, 100, 150) # Full strength, 100ms, 150Hz carrier ``` ### `haptic_strength([value])` Get or set the default motor strength used by `haptic_pulse()`. * `value` (optional): New strength (0–255). If omitted, just returns current. * Returns the current strength (0–255). **Example:** ```jython print(haptic_strength()) # Read current haptic_strength(200) # Set to 200 ``` ### `haptic_off()` Immediately stop the motor and cancel any active pulse. ### `tone(freq_hz, [duration_ms], [duty])` Play an audible tone from the motor coil. At very low duty cycles (~30/255), the coil doesn't spin but vibrates audibly at the PWM frequency. * `freq_hz`: Tone frequency in Hz. * `duration_ms` (optional): Duration in milliseconds. `0` or omitted = play until `no_tone()`. * `duty` (optional): Duty cycle (0–255). Default 30. **Example:** ```jython import time # Play a scale for freq in [262, 294, 330, 349, 392, 440, 494, 523]: tone(freq, 200) time.sleep(0.25) # Continuous tone until stopped tone(440) time.sleep(2) no_tone() ``` ### `no_tone()` Stop the currently playing tone. ### `tone_playing()` Check if a tone is currently playing. * Returns `True` if a tone is active. --- ## IR Send/Receive Infrared communication over a modified NEC protocol using the onboard IR LED (TX) and TSOP receiver (RX). The RMT-driven encoder automatically prepends an NEC leader, packs each payload word, appends a CRC32, and emits a trailing pulse — MicroPython just picks the payload. Two payload shapes are available: * **Classic 1-byte/1-byte frames** via `ir_send(addr, cmd)` / `ir_read()`. A single 32-bit NEC word `(~addr, addr, ~cmd, cmd)`, ~110 ms on wire. * **Multi-word frames** via `ir_send_words(words)` / `ir_read_words()`. 1–64 raw 32-bit words (up to 256 bytes of payload) with an appended CRC. Each data word adds ~54 ms, so a 3-word frame ≈ 230 ms on wire. The Boop screen and MicroPython share the same RMT hardware; `ir_start()` brings the radio up for Python use and waits for it to be ready before returning, so the first `ir_send*()` after `ir_start()` will not race. ### `ir_send(addr, cmd)` Transmit a single classic NEC frame. * `addr`: NEC address byte (0–255). * `cmd`: NEC command byte (0–255). Blocks briefly while the RMT hardware streams the frame. Returns `0` on success; raises `OSError` if the IR hardware is down (e.g. called before `ir_start()`). ### `ir_start()` Bring the IR hardware up and start receiving in Python mode. Incoming frames are queued in an 8-slot ring buffer for retrieval with `ir_read()` or `ir_read_words()`. `ir_start()` blocks up to ~500 ms while Core 0 powers the RMT channel, and drains any stale frames left over from the Boop screen. ### `ir_stop()` Stop Python IR RX and drop any queued frames. Does **not** power down the hardware — the Boop screen may still use it. ### `ir_available()` Check if at least one classic NEC frame is waiting in the RX queue. * Returns `True` if `ir_read()` would return a frame. ### `ir_read()` Pop one classic NEC frame from the queue. * Returns `(addr, cmd)` if a frame is available, or `None` if the queue is empty. **Example:** ```jython import time ir_start() oled_println("Listening for IR...") while True: if ir_available(): frame = ir_read() if frame: addr, cmd = frame oled_clear() oled_println("IR: " + hex(addr) + " " + hex(cmd)) haptic_pulse() time.sleep(0.05) ``` ### `ir_send_words(words)` Transmit a multi-word NEC frame. The encoder automatically emits a leader, then each word LSB-first, then a CRC32 over the payload. * `words`: A list, tuple or other sequence of 1–64 integers. Each element is converted to an unsigned 32-bit value. * Raises `ValueError` if the sequence is empty or longer than 64 words. * Raises `OSError(1)` if the IR hardware is not up (e.g. before `ir_start()`). **Example:** ```jython ir_start() ir_send_words([0xB0, 0xDEADBEEF, 0x12345678]) ``` ### `ir_read_words()` Read one received multi-word NEC frame. * Returns a `tuple` of up to 64 integers (the payload words, CRC-validated and stripped by the decoder) if a frame is available. * Returns `None` if the RX queue is empty. ### `ir_flush()` Drop every pending RX frame (both classic and multi-word queues). Safe to call at any time; a no-op if the IR hardware is down. ### `ir_tx_power([percent])` Get or set the IR carrier duty cycle. A higher duty drives the IR LED harder and extends range at the cost of current and LED stress. * `percent` (optional): New duty cycle in the range **1–50** (percent of the 38 kHz carrier period). Call with no argument to just read the current value. Power-on default is **50**. * Returns the current duty as an integer percent after the (optional) set. * Raises `ValueError` if `percent` is outside 1–50. **Example:** ```jython ir_start() print("Default duty:", ir_tx_power()) # 50 ir_tx_power(10) # throttle down for self-loopback ``` --- ## Badge Identity & Boops ### `my_uuid()` Return this badge's globally unique identifier, derived from the ESP32-S3 eFuse `OPTIONAL_UNIQUE_ID` (first 6 bytes, hex-encoded). * Returns a 12-character lowercase hex string, e.g. `"a1b2c3d4e5f6"`. ### `boops()` Return the on-flash `/boops.json` contents as a string. This is the same document that the Boop screen maintains — each completed boop is appended with the peer UID, peer name / ticket (if the server backfilled them), and a `status` field (`"ok"`, `"local"`, etc.). * Returns a JSON string. When no boops have been recorded yet, returns `'{"pairings":[]}'` so callers can always `json.loads()` the result. **Example:** ```jython import json data = json.loads(boops()) for p in data.get("pairings", []): oled_println(p.get("peer_badge_uid", "?")) oled_show() ``` --- ## Script Control ### `exit()` Cleanly stop the currently running MicroPython app by raising `SystemExit`. Prefer this over `sys.exit()` so the host runtime sees the same exception type the rest of the firmware expects. Holding all four face buttons for about 1 second also force-exits the running app from outside. ### `dev(*args)` *(dev builds only)* Variadic string-argument dispatcher for the firmware test harness. Only available in builds with `BADGE_ENABLE_MP_DEV` (e.g. the `echo-dev` PlatformIO environment). Each argument is coerced to a string and forwarded to the C++ runtime. Returns a string result. --- ## Filesystem Access The badge's VFS is mounted and accessible through the standard `os` module. Apps can read and write files for saving state, high scores, or configuration. ```jython import os os.listdir("/apps") os.listdir("/") with open("/apps/my_app/save.json", "w") as f: f.write('{"score": 42}') with open("/apps/my_app/save.json", "r") as f: data = f.read() ``` Available `os` operations: `listdir`, `mkdir`, `remove`, `rename`, `stat`, `getcwd`, `chdir`, `ilistdir`, `statvfs`. Standard `open()` / `read()` / `write()` / `close()` work as expected. --- ## Mouse Overlay Hardware-composited cursor overlay for building GUIs and games. When enabled, a cursor sprite is automatically drawn on top of the OLED framebuffer at every display refresh. The joystick controls cursor position and face buttons generate click events — all handled asynchronously in the service pump so Python code only needs to query position and clicks. ### `mouse_overlay(enable)` Enable or disable the cursor overlay. * `enable`: `True` to enable, `False` to disable. When enabled, the joystick moves the cursor and button presses are captured as click events (instead of being consumed by `button_pressed()`). **Example:** ```jython mouse_overlay(True) mouse_set_pos(64, 32) while True: oled_clear() oled_set_cursor(0, 0) oled_print("x:" + str(mouse_x()) + " y:" + str(mouse_y())) oled_show() btn = mouse_clicked() if btn == BTN_RIGHT: oled_println("Clicked!") if btn == BTN_LEFT: break time.sleep_ms(30) mouse_overlay(False) ``` ### `mouse_set_bitmap(data, w, h)` Set a custom cursor sprite. Format is a packed 1-bit-per-pixel bitmap, row-major, **MSB-first within each byte** (the leftmost pixel of a row is bit 7 of its first byte). Each row is padded to a whole number of bytes, so `ceil(w / 8) * h` bytes are read from `data`. * `data`: `bytes` or `bytearray` containing the bitmap. * `w`: Width in pixels (1–32). Values larger than 32 are clamped to 32. * `h`: Height in pixels (1–32). Values larger than 32 are clamped to 32. The internal cursor buffer is 128 bytes, so any `(w, h)` with `ceil(w / 8) * h ≤ 128` is accepted. A 32×32 sprite uses the entire buffer. The hot-spot is automatically set to the sprite's center. The default cursor is an 8×8 arrow pointer. **Example:** ```jython # 8x8 crosshair cursor crosshair = bytes([ 0b00010000, 0b00010000, 0b00010000, 0b11101110, 0b00010000, 0b00010000, 0b00010000, 0b00000000, ]) mouse_set_bitmap(crosshair, 8, 8) ``` ### `mouse_x()` Returns the current cursor X position (0–127). ### `mouse_y()` Returns the current cursor Y position (0–63). ### `mouse_set_pos(x, y)` Warp the cursor to an absolute position. * `x`: X position (clamped to 0–127). * `y`: Y position (clamped to 0–63). ### `mouse_clicked()` Read the last click event. Returns the button ID (`BTN_RIGHT`, `BTN_DOWN`, `BTN_LEFT`, `BTN_UP`) or `-1` if no click is pending. **Consumes** the event on read — calling again returns `-1` until a new button is pressed. ### `mouse_set_speed(speed)` Set the cursor movement speed (only affects relative mode). * `speed`: Pixels per joystick poll at full deflection (1–20). Default is 3. **Example:** ```jython mouse_set_speed(5) # faster cursor mouse_set_speed(1) # very precise ``` ### `mouse_set_mode(mode)` Switch between absolute and relative positioning. * `mode`: `MOUSE_ABSOLUTE` (joystick position = cursor position) or `MOUSE_RELATIVE` (joystick deflection = cursor velocity). Default is `MOUSE_RELATIVE`. In **absolute** mode the cursor tracks the joystick 1:1 — stick center is screen center. In **relative** mode the joystick acts like a mouse — deflect to move, release to stop. Use `mouse_set_speed()` and `mouse_set_pos()` to tune relative mode. **Example:** ```jython mouse_set_mode(MOUSE_RELATIVE) mouse_set_pos(64, 32) mouse_set_speed(4) ``` --- ## Constants Reference ### Buttons | Constant | Value | Description | |----------|-------|-------------| | `BTN_RIGHT` | 0 | Right button | | `BTN_DOWN` | 1 | Down button | | `BTN_LEFT` | 2 | Left button | | `BTN_UP` | 3 | Up button | | `BTN_CIRCLE` | 0 | PlayStation-style alias for right | | `BTN_CROSS` | 1 | PlayStation-style alias for down | | `BTN_SQUARE` | 2 | PlayStation-style alias for left | | `BTN_TRIANGLE` | 3 | PlayStation-style alias for up | | `BTN_CONFIRM` | 4 | Semantic confirm/select, follows `swap_ok` | | `BTN_SAVE` | 4 | Semantic save/apply, follows `swap_ok` | | `BTN_BACK` | 5 | Semantic back/cancel, follows `swap_ok` | | `BTN_PRESETS` | 3 | Semantic alias for preset/actions | ### LED Matrix Images | Constant | Value | Description | |----------|-------|-------------| | `IMG_SMILEY` | `"smiley"` | Smiley face | | `IMG_HEART` | `"heart"` | Heart shape | | `IMG_ARROW_UP` | `"arrow_up"` | Upward arrow | | `IMG_ARROW_DOWN` | `"arrow_down"` | Downward arrow | | `IMG_X_MARK` | `"x_mark"` | X mark | | `IMG_DOT` | `"dot"` | Center dot | ### LED Matrix Animations | Constant | Value | Description | |----------|-------|-------------| | `ANIM_SPINNER` | `"spinner"` | Rotating spinner | | `ANIM_BLINK_SMILEY` | `"blink_smiley"` | Blinking smiley face | | `ANIM_PULSE_HEART` | `"pulse_heart"` | Pulsing heart | ### Mouse Overlay Modes | Constant | Value | Description | |----------|-------|-------------| | `MOUSE_ABSOLUTE` | 1 | Joystick position = cursor position | | `MOUSE_RELATIVE` | 0 | Joystick deflection = cursor velocity (default) | --- # Glossary of Terms ## Basic Concepts `net` = a group of all the `node`s that are connected together (enter `n` to see the list) `node` = anything the crossbar array can connect to, which includes everything on the breadboard and Nano header, as well as the internal `special function` `node`s like `routable GPIO`, `ADC`s, `DAC`s `row` = *kinda* the same thing as `node` but I generally use it to mean stuff on the breadboard (so special function things like `routable GPIO`, `ADC`s, `DAC`s that don't have a set location are excluded) `rail` = I use this to refer to the 4 horizontal power rails on the top and bottom (`top_rail`, `bottom_rail`, `gnd`), I will never call a vertical `row` a `rail`. (I know they're columns but it's easier to say a lot) `bridge` = a pair of exactly two `node`s (this is what you're making when you connect stuff with the probe, enter `b` to see the bridge array) `path` = the set of crossbar connections needed to make a single `bridge`, so it can have multiple `hop`s if it doesn't have a direct connection and needs to make a `bounce` through an intermediate `chip` (enter `c` to see the crossbar array) ## Hardware `chip` = shorthand for the CH446Qs specifically, lettered A-L. The first 8 (A-H) are considered "breadboard `chips`", and the last 4 (I-L) are considered "special function" chips (enter `c` to see their connections) `menu` = I generally mean the onboard clickwheel `menu` when I say this (`click` the wheel to enter those and `scroll` around.) Sometimes I mean the `main menu` which is the list of single character command that gets presented over serial. ## Slots and Files `slot` = one of **8** saved circuit configurations (slots 0-7) that you can switch between. Use `<` to cycle forward through slots, or use the menus to jump to a specific slot. The **active slot** is the one currently loaded and affecting the hardware. `slot file` = a YAML file on the filesystem that stores a complete circuit configuration including bridges, power settings, and colors. Located at `/slots/slotN.yaml` where N is 0-7. These files are human-readable and can be edited directly! `active slot` = the currently loaded slot. Only the active slot affects the hardware. Use `Q` command to query which slot is active. When you make connections with the probe, they're saved to the active slot automatically. ## Slot Management Commands - `<` = cycle to next slot (0→1→2...→7→0) - `Q` = query which slot is currently active (returns `ACTIVE_SLOT:X`) - `Y` = print YAML ## YAML Format Slot files use YAML format with named nodes for readability: ```yaml bridges: - {n1: 1, n2: 10, dup: 2, color: red} - {n1: NANO_D5, n2: GP_1, dup: 2} power: topRail: 3.30 bottomRail: 2.50 ``` **Named nodes:** `NANO_D0-D13`, `NANO_A0-A7`, `GP_1-8` (or `RP_GPIO_1-8`), `TOP_RAIL`, `BOTTOM_RAIL`, `GND`, `DAC0`, `DAC1`, etc. (Note these differ from the MicroPython constants - `GPIO_1` and `DAC0_5V` won't parse in slot files.) You can view and edit these files in the [File Manager](08-file-manager.md) or via USB Mass Storage mode (`U` command). ## Wokwi Import `W` = Import circuit from [Wokwi](https://wokwi.com) simulator 1. Design circuit on wokwi.com 2. Copy `diagram.json` content 3. Type `W` in Jumperless 4. Paste JSON content 5. Circuit is converted and saved to active slot The parser automatically maps Wokwi breadboard pins, Arduino Nano pins, and logic analyzer channels to Jumperless nodes, and preserves your wire colors from Wokwi! ---