This tree builds tiny Buildroot-based Linux images for RP2350 RISC-V (Hazard3) boards. The original target is the Raspberry Pi Pico 2 / SparkFun Pro Micro RP2350 work; this branch adds an Adafruit Fruit Jam RP2350B bring-up target.
For a community getting-started walkthrough, see Linux on Fruit Jam by Mikey Sklar on Adafruit Playground.
Flash the Fruit Jam UF2, then use the image from a serial console first. On the
tested macOS host the USB CDC console appears as /dev/cu.usbmodem1101; the
hardware UART header remains available at 115200 8N1 on GPIO8/GPIO9.
screen /dev/cu.usbmodem1101 115200Once logged in, these commands show what the image can do:
fruitjam-services status
ls /root/berry /root/sh /root/rtttl
sh /root/sh/run-all.sh
berry-run /root/berry/run-all.be
berry-run /root/berry/run-visual.be
fruitjam-rtttl scaleThe examples are intentionally part of the image:
/root/berrycontains Berry language and hardware examples, including the importablefruitjamhardware module, NeoPixel animations, USB HID decode, and direct sysfs/ADC/button reads./root/shcontains shell examples for services, HTTP/CGI, NeoPixels, buttons, I2C, ADC, SD, DVI, USB-host status, WAV analysis, and small utility checks./root/rtttlcontains RTTTL tunes for the TLV320DAC3100 audio bring-up path.
To make the board reachable from another machine over the onboard ESP32-C6 AirLift, put WiFi credentials on the SD card and reboot:
mkdir -p /mnt/sd/fruitjam
chmod 700 /mnt/sd/fruitjam
cat > /mnt/sd/fruitjam/wifi.conf <<'EOF'
SSID=your-ssid
PASSWORD=your-passphrase
EOF
chmod 600 /mnt/sd/fruitjam/wifi.confAfter AirLift joins WiFi, airliftctl serve-inbound exposes the user-facing
network surface directly through the coprocessor:
http://<board-ip>/serves/mnt/sd/www/index.htmlfor user pages.http://<board-ip>/playgroundopens the built-in hardware playground.telnet <board-ip> 23opens the tiny Fruit Jam shell.- Passive FTP on
<board-ip>:21serves the SD card rooted at/mnt/sd, so/wavsis visible to FTP clients.
This is not a Linux wlan0 interface. It is a small AirLift/NINA userspace
server for HTTP, telnet, and FTP; normal Linux sockets and the target-side
wget still use the local loopback network.
The Fruit Jam target is named adafruit_fruit_jam_rp2350. It uses:
- RV32 RISC-V Hazard3, no MMU.
- SMP disabled.
- BusyBox-centered userspace plus small no-MMU-friendly Fruit Jam tools.
- CramFS root filesystem in flash, with a 10 MiB flash partition reserved from 6 MiB through the end of the 16 MiB device.
- Kernel copied to 8 MiB external PSRAM.
- UART1 console on the Fruit Jam
TX/RXpins (GPIO8/GPIO9), 115200 8N1. - USB CDC ACM console on
/dev/ttyGS0when the gadget enumerates. - USB CDC shells use standalone
/usr/bin/hush. The hardware UART usesfruitjam-uart-loginto wait for Enter before execinghush, avoiding a no-MMU respawn loop when nothing is attached to GPIO8/GPIO9. Telnet uses a tinyfruitjam-telnetdplus/usr/bin/fruitjam-shellso remote login does not require a large shell allocation, with a four-entry command history and command/path tab completion for telnet sessions. The experimental USB boot-keyboard shell has matching small history and command/path completion. - Berry installed as
/usr/bin/berry, including-e, script execution, and REPL. - BusyBox
vi. - PIO-backed
/dev/neopixelsfor the five onboard NeoPixels, plus/root/berry/neopixels.beas a small Berry smoke-test program. - Legacy sysfs GPIO through
gpiochip0for GPIO0..GPIO45, including GPIO29 LED output and GPIO0/GPIO4/GPIO5 button inputs. - microSD as
/dev/mmcblk0//dev/mmcblk0p1, mounted at/mnt/sdas VFAT. fruitjam-buttonsdaemon watching GPIO0/GPIO4/GPIO5 and logging events to/mnt/sd/fruitjam/buttons.db.fruitjam-buttonlog, a tiny fixed-schema SQLite 3 file writer. It avoids the full SQLite CLI/library because the no-MMU target cannot reliably allocate a large SQLite-linked process after services are running.- Tiny
mosquitto_pubandmosquitto_subcompatible MQTT tools. They use normal Linux sockets by default, support username/password authentication for QoS 0 broker smoke tests, and--airliftroutes the same requests throughairliftctl mqtt-pub/mqtt-subover the ESP32-C6 NINA TCP socket API. - GPIO-backed Linux I2C master on GPIO20/GPIO21, exposed as
/dev/i2c-0, plus the tinyfruitjam-i2cscan/ping helper. /dev/fruitjam-audiostarts/stops PIO1-generated TLV320DAC3100 MCLK, BCLK, WS, and DIN signals on GPIO24-GPIO27.fruitjam-rtttlis the current audio bring-up helper and has microphone-confirmed audible RTTTL output.fruitjamctlGPIO diagnostics for USB-host power enable, shared TLV320/ESP32-C6 reset, and software BOOTSEL.airliftctldiagnostics for the onboard ESP32-C6 AirLift NINA coprocessor, including firmware/status/IP checks and a minimal outbound TCP HTTP fetch. AirLift SPI access is locked so a secondairliftctlexits safely while the inbound service owns the coprocessor; read-onlyfw,mac,status, andipcommands fall back to the last values in/tmp/airlift-start.log.
Validated on hardware:
- USB CDC shell on
/dev/cu.usbmodem1101. - Hardware UART shell/log path on
/dev/cu.usbserial-P97cvdxp. /proc/mtdreportsmtd0: 00a00000 00001000 "cramfs"./sys/class/gpio/gpio29/valuereads back output writes, and GPIO0/GPIO4/GPIO5 export as pulled-up inputs.- AirLift inbound HTTP serves user pages from
/mnt/sd/wwwathttp://<board-ip>/and the built-in hardware playground athttp://<board-ip>/playgroundafter WiFi is configured on the SD card. - The optional loopback
fruitjam-services coreset can servehttp://127.0.0.1/,http://127.0.0.1/playground, and CGI scripts; the tiny HTTP-onlywget -O -implementation returns the CGI environment test. fruitjam-services initcreates/mnt/sd/www/index.htmlwith a small "Fruit Jam stuff" placeholder only when the SD card does not already have an index page. Users can replace that file with their own web page.- The
/playgroundpage is a self-contained reactive Fruit Jam hardware playground./cgi-bin/fruitjam.cgiexposes JSON actions for status, NeoPixels, button test events, RTTTL playback, WAV listing/playback, ADC reads, DVI rendering, USB-host status, I2C checks, explicit Berry example runs, and regular user.befiles directly under/mnt/sd/berry. Hardware actions use direct C/tiny-helper paths; Berry is only invoked by the Berry runner. - AirLift inbound telnet accepts a remote shell and echoed
TELNET_OK. AirLift telnet sessions use a 60-second default idle timeout so a stale client does not monopolize the single no-MMU shell bridge; a new connection can preempt a stale idle session after 15 seconds. - Default AirLift startup is wrapped by
fruitjam-services airlift-monitor, which reruns the inbound server if it exits after leaving ESP32-C6 TCP sockets open, removes stale monitor PID files, and treats a missing inbound heartbeat for 60 seconds as a restart condition. The inbound server also recycles itself periodically and exits on listener accept failures so the monitor can reopen NINA sockets. Check/tmp/airlift-start.logfor probe/join and restart messages. fruitjam-services statusreports service processes and TCP/UDP listeners without spawningps/netstat, avoiding the no-MMU fragmentation regression that previously broke CGI after status checks.- AirLift inbound passive FTP lists, uploads, and downloads files under
/mnt/sd. The optional loopback core service set also includes tiny FTP with PASV/EPSV and PORT/EPRT data modes plus TFTP daemons for target-side smoke tests. /proc/partitionsreportsmmcblk0andmmcblk0p1;/mnt/sdis VFAT, writable, unmountable, and remountable.- Synthetic button events written to
/run/fruitjam-buttons.fifoare logged to/mnt/sd/fruitjam/buttons.dband mirrored in/mnt/sd/fruitjam/buttons.log. - With
/mnt/sd/fruitjam/buttons.confset toMQTT_TRANSPORT=airlift, a synthetic button event published throughmosquitto_pub --airliftand was received by a test MQTT broker asfruitjam/buttons/button2. /dev/i2c-0isi2c-gpio0on GPIO20/GPIO21;fruitjam-i2c scanfound the TLV320DAC3100 control interface at0x18./sys/class/misc/fruitjam-audioregisters as misc char device10:125, and/dev/fruitjam-audioacceptsstart/stop;fruitjam-rtttl --loud --tone 880 2500was verified with the Mac microphone helper.berry -e 'print("berry ok")'.- Berry REPL expression evaluation.
berry-run /root/berry/neopixels.be, visually confirmed on the onboard NeoPixels./bin/vi -> busybox.fruitjamctl bootsel, verified bypicotool info -areportingboot type: bootsel.- AirLift over RP2350 PL022/spidev hardware SPI:
airliftctl probereported NINA firmware3.3.0, WiFi joined successfully, andairliftctl tcp-get example.com /returned HTTP 200 plus the response body.airliftctl mqtt-pubpublished a QoS 0 MQTT message over the same NINA TCP path to a local broker, which receivedfruitjam/test hello-from-airlift.mosquitto_subandairliftctl mqtt-subare now present for finite subscribe smoke tests, pending hardware verification on the current board.
AirLift is exposed as a userspace NINA socket helper, not as a Linux wlan0
network interface. The boot service starts airliftctl serve-inbound for
external HTTP, telnet, and FTP when WiFi is available. The tiny target-side
wget still uses normal Linux sockets, while airliftctl tcp-get,
mosquitto_pub --airlift, and mosquitto_sub --airlift use the ESP32-C6
directly.
Status means support in this Buildroot Linux image, not support in CircuitPython. Pin details come from the Adafruit Fruit Jam pinout and the local Fruit Jam pin map in docs/pinmap-fruitjam.md.
| Hardware | Pins / device | Status | Current Linux support | TODO / notes |
|---|---|---|---|---|
| RP2350B RISC-V CPU | Hazard3 RV32 | Supported | Boots Linux 6.15 no-MMU with SMP disabled. | Continue reducing memory pressure from larger userland tools. |
| External flash | 16 MiB QSPI | Supported | Bootloader, kernel, DTB, and 10 MiB CramFS rootfs live in flash. | None for current boot path. |
| External PSRAM | 8 MiB QMI/CS1 GPIO47 | Supported | Kernel is copied to PSRAM by the bootloader. | No general Linux RAM expansion beyond current port design. |
| USB device console | RP2350 USB CDC, /dev/ttyGS0 |
Supported | Host sees /dev/tty.usbmodem1101; CDC shell is used by the smoke suite. |
CDC console can be sensitive to repeated open/close churn. |
| UART console header | GPIO8 TX, GPIO9 RX | Supported | Hardware UART shell/log path at 115200 8N1. | Keep as fallback when USB or graphics work changes. |
| ROM BOOTSEL from Linux | RP2350 reboot command | Supported | fruitjamctl bootsel re-enters BOOTSEL and picotool info -a sees the ROM. |
None. |
| BusyBox userspace | /bin, /usr/bin |
Supported | BusyBox tools plus vi, hush, web/network helpers, free/uptime/du/fruitjam-mem, tiny ps/pgrep/pkill, and Fruit Jam tools. |
Keep applets constrained for no-MMU allocation behavior. |
| Berry interpreter | /usr/bin/berry, /usr/bin/berry-run, /root/berry/fruitjam.be |
Supported | berry -e, scripts, REPL, and an importable Fruit Jam hardware module for GPIO/buttons, ADC, I2C scan/ping, USB-host status through the kernel bridge when present, USB HID report decode, USB keyboard command helpers, AirLift diagnostics/TCP helpers, MQTT command helpers, device presence, audio clock, RTTTL/WAV command helpers, fruitjamctl board-control commands, DVI command writes, and NeoPixels; berry-run /root/berry/neopixels.be drives LEDs with lower cache pressure for multi-script runs. The playground can also list and run regular .be files from /mnt/sd/berry as SD: entries. |
Extend the Berry module as more kernel helpers land. |
| Onboard NeoPixels | GPIO32, five LEDs | Supported | PIO-backed /dev/neopixels; Berry and CGI can update pixels. |
None for basic color writes. |
| Buttons | GPIO0, GPIO4, GPIO5 | Supported | Sysfs GPIO input, fruitjam-buttons, button log, CGI status, synthetic test events. |
Physical edge logging should get longer soak testing. |
| Red LED / IR pin | GPIO29 | Partial | LED control through sysfs/fruitjamctl; active-low output works. |
IR receiver decoding is not implemented. |
| microSD card | GPIO34-39 SPI/MMC, CD GPIO33 | Supported | /dev/mmcblk0p1 mounts at /mnt/sd as VFAT; read/write/remount verified. |
Optional wider SDIO DAT1/DAT2 path is not implemented. |
| I2C / STEMMA QT | GPIO20 SDA, GPIO21 SCL | Supported | i2c-gpio0 as /dev/i2c-0; fruitjam-i2c scan finds TLV320 at 0x18. |
Add kernel descriptions for more attached devices as needed. |
| ADC / analog header | GPIO40-GPIO47 ADC | Supported | fruitjam-adc reads ADC channels and internal temperature through sysfs. |
Calibration/scale polish. |
| TLV320DAC3100 audio | I2C 0x18, GPIO24 DIN, GPIO25 MCLK, GPIO26 BCLK, GPIO27 WS |
Partial | /dev/fruitjam-audio generates PIO clocks; fruitjam-rtttl configures the codec and microphone-verified RTTTL output works. |
Full streamed PCM/I2S playback is not implemented. |
| ESP32-C6 AirLift | SPI GPIO28/30/31/46, READY GPIO3, IRQ GPIO23, reset GPIO22 | Partial | NINA firmware 3.3.0; airliftctl can probe, scan, join WiFi, TCP GET, MQTT publish, and serve inbound HTTP/telnet/FTP. |
Not a Linux wlan0 interface yet; normal wget still only uses Linux sockets. |
| HTTP user pages and playground | AirLift TCP/80; optional loopback HTTP | Supported | / serves /mnt/sd/www/index.html; /playground serves the built-in hardware UI; /cgi-bin/fruitjam.cgi reports status and controls NeoPixels, RTTTL, WAVs, I2C, ADC, DVI, USB-host status plus bounded keyboard probes, buttons, built-in Berry examples, and user Berry files from /mnt/sd/berry. Hardware actions use direct C/tiny-helper paths. |
Use http://<board-ip>/playground after AirLift joins WiFi, or fruitjam-services core for loopback tests. |
| Telnet service | AirLift TCP/23; optional loopback TCP/23 | Supported | AirLift inbound shell and tiny fruitjam-telnetd/fruitjam-shell; telnet smoke tests pass. |
Only one AirLift telnet session at a time. |
| FTP service | AirLift TCP/21 plus passive data 2121+; optional loopback TCP/21 | Supported | AirLift passive FTP lists, uploads, and downloads files under /mnt/sd; FileZilla passive mode works. The optional loopback fruitjam-ftpd supports PASV/EPSV and PORT/EPRT, plus STOR/APPE/RETR/LIST/NLST/RNFR/RNTO/MKD/RMD/DELE. |
Upload completion can be slow over the current NINA raw socket path. |
| TFTP service | Optional loopback UDP/69 | Supported | BusyBox tftpd serves the TFTP area README when fruitjam-services core is started. |
Not part of the default external AirLift service set. |
| USB host data | GPIO1 D+, GPIO2 D- | Experimental | USB host 5V can be switched, /dev/fruitjam-usbhost owns line state, reset timing, and PIO packet diagnostics, and the Linux HCD is configured with the wili8jam PIO0/SM0/SM1/SM2/DMA9/252 MHz settings. The HCD now auto-registers after an 8 second boot service window, power-cycles host 5V before registration, waits 500 ms after port reset before EP0 enumeration for the CH334F hub, and schedules an automatic power-cycle recovery after repeated EP0 failures. fruitjam-usbhost hcd-start remains available as an idempotent retry path. Kernel config enables hub, HID, evdev/joydev, and xpad/Xbox support. |
Flash the current image and prove Logitech receiver plus Xbox360 gamepad enumeration with ./scripts/usbhost-hcd-smoke.py; live current-board proof is still required. |
| USB host 5V switch | GPIO11 | Partial | fruitjamctl usb-power on/off controls power enable. |
Keep power/reset behavior aligned with the Linux HCD path. |
| DVI / HSTX output | GPIO12-GPIO19, /dev/fruitjam-dvi |
Partial | Tiny RGB332 HSTX DVI misc driver plus fruitjam-dvi text/dashboard/helper command output; hardware commands were verified on the flashed image. |
Full fbdev/console is not implemented. |
| I2S data path | GPIO24 DIN, GPIO25 MCLK | Partial | Tiny generated-tone path exists for RTTTL and simple WAV tone tests. | Add complete streamed PCM driver/path. |
| IR receiver | GPIO29 shared with LED | TODO | No decoder. | Decide conflict/ownership with red LED. |
| ESP32-C6 native flashing from Linux | SPI/UART control path | TODO | Updated externally using Adafruit updater workflow. | Integrate a controlled in-image updater later. |
The target keeps tools intentionally small because this is a no-MMU system where process size and memory fragmentation matter.
| Tool or path | Purpose |
|---|---|
fruitjam-services |
Start, stop, restart, and inspect Fruit Jam services without large ps/netstat helpers. |
fruitjam-telnetd |
Tiny TCP/23 telnet shell service. |
fruitjam-shell |
Tiny command shell used by telnet sessions, with recent command history and command/path tab completion. |
fruitjam-ftpd |
Tiny FTP server rooted at /mnt/sd, with passive and active data modes for loopback service tests. |
BusyBox httpd |
Local web server and CGI runner under /www. |
/cgi-bin/fruitjam.cgi |
JSON hardware playground API using direct C/tiny-helper hardware actions, bounded USB keyboard probes, plus an explicit Berry runner. |
BusyBox tftpd |
TFTP server rooted at /tmp/tftp. |
wget |
Tiny HTTP-only client for normal Linux IPv4 sockets. |
telnet |
Tiny telnet client. |
nc |
Tiny bidirectional netcat-style TCP client/server. |
/root/sh/serial-over-tcp.sh |
Bridges a character device to a TCP port with nc. |
fruitjamctl |
Board diagnostics, reset controls, USB-host power, and software BOOTSEL. Berry exposes command builders and run wrappers for these board controls. |
fruitjam-i2c |
Scan or ping the GPIO-backed /dev/i2c-0 bus. |
fruitjam-adc |
Read RP2350 ADC inputs and the internal temperature ADC channel. |
fruitjam-dvi |
Render bounded text/dashboard/test frames to /dev/fruitjam-dvi. |
fruitjam-wavplay |
Analyze simple WAV files and play tone segments through the TLV320 tone path. |
fruitjam-usbhost |
Report USB host power and D+/D- line state, preferring /dev/fruitjam-usbhost when present, with json, wait, monitor, reset, decode, hid, parameterized kbd-init/kbd-poll, kbd-find, kbd-text/kbd-events, and kbd-shell/kbd-auto-shell boot-keyboard commands. The USB keyboard shell has a four-entry history and command/path tab completion. Berry exposes command builders and run wrappers for these keyboard probes. |
fruitjam-hidkeys |
Decode USB HID boot-keyboard 8-byte reports into text/events, including DATA0/DATA1 last_rx_hex packets from the PIO bridge when they contain an 8-byte keyboard report. |
fruitjam-mem, free |
Tiny no-fork memory, uptime, load, and commit-pressure summary from /proc. |
fruitjam-uptime, uptime |
Tiny no-fork uptime and load-average view from /proc/uptime and /proc/loadavg, with text and JSON output. |
fruitjam-du, du |
Tiny recursive disk-usage helper with summary/all/byte/json modes for SD/web/Berry file inspection. |
fruitjam-ps, ps |
Tiny no-fork process list from /proc, with text and JSON output. |
fruitjam-pgrep, pgrep, pkill |
Tiny no-fork process lookup/signaling from /proc using substring or exact matching, avoiding `ps |
fruitjam-buttons |
Button daemon for GPIO0/GPIO4/GPIO5 with log, FIFO, SQLite, and MQTT hooks. |
fruitjam-buttonlog |
Dump the fixed-schema button SQLite 3 log. |
fruitjam-rtttl |
Play a small RTTTL tune through the TLV320 PIO I2S tone path. |
airliftctl |
ESP32-C6 NINA firmware, WiFi, TCP, and MQTT diagnostics over SPI, including mqtt-pub and finite mqtt-sub. |
mosquitto_pub |
Tiny MQTT publisher with optional -u/-P; add --airlift to publish through the ESP32-C6. |
mosquitto_sub |
Tiny MQTT subscriber with optional -u/-P, -C, -W, and -v; add --airlift to subscribe through the ESP32-C6. |
berry |
Berry interpreter with -e, scripts, and REPL. |
berry-run |
Tiny wrapper that drops page cache before execing Berry for steadier multi-script runs. |
/dev/neopixels |
PIO-backed device for the five onboard NeoPixels. |
BusyBox vi |
On-target text editor. |
/ is a read-only CramFS root. Use /tmp for temporary files and /mnt/sd for
persistent data. The SD card is mounted as VFAT at /mnt/sd when present.
From macOS, run the automated smoke suite against the USB CDC Linux console:
./scripts/cdc-smoke-test.pyThe default port is /dev/tty.usbmodem1101; override it with --port or
FJ_CDC_PORT. The suite keeps one CDC shell open and checks Berry, board
helpers, buttons, I2C, NeoPixels, loopback HTTP/Telnet/TFTP/FTP, and AirLift.
WiFi join and AirLift TCP tests run when credentials are supplied with either
--ssid/--password, environment variables, or the ignored local env file:
cat > .fruitjam.env <<'EOF'
export FJ_WIFI_SSID=your-ssid
export FJ_WIFI_PASSWORD=your-passphrase
EOF
chmod 600 .fruitjam.env
./scripts/cdc-smoke-test.pyUse --audio to include the short RTTTL speaker test. Use --usb-keyboard
with a boot-protocol HID keyboard plugged into the Fruit Jam host port to run
kbd-find, the Berry USB keyboard helper, live kbd-auto-text/kbd-auto-events
loops, and the kbd-auto-shell helper shell. Add
--usb-keyboard-require-input when someone can press keys during the test and
you want captured text/events plus a typed echo USBKBD_SMOKE shell command to
be required. Use --skip-airlift or --skip-services to narrow the suite
during bring-up.
For the USB-host keyboard milestone alone, the focused runner can use either the CDC shell or the AirLift telnet shell:
./scripts/usbhost-keyboard-smoke.py --transport cdc --port /dev/tty.usbmodem1101 --require-input
./scripts/usbhost-keyboard-smoke.py --transport telnet --telnet-host <board-ip> --require-inputThe runner first checks that command markers round-trip through the selected
board shell, then proves the current narrow boot-protocol keyboard path:
kbd-find, the Berry helper, live text/events, and a typed command in
kbd-auto-shell. The helper shell supports up/down history recall and tab
completion for commands and paths.
Recent release-prep verification on June 12, 2026:
./scripts/cdc-smoke-test.py
./scripts/validate-fruitjam-examples.sh
./scripts/validate-fruitjam-image.sh buildroot-output-docker-imagesThe CDC suite is the broad regression entrypoint. Release-prep hardware checks also included direct AirLift HTTP, remote telnet, passive FTP list/upload/download and microphone-verified RTTTL/WAV audio on the flashed image.
| Smoke test | Result | Evidence |
|---|---|---|
| USB CDC shell | PASS | fruitjam-cdc-ok over /dev/tty.usbmodem1101. |
| Kernel boot | PASS | Linux 6.15.0 no-MMU Buildroot console responded. |
| Tool inventory | PASS | airliftctl, berry, fruitjamctl, fruitjam-i2c, fruitjam-rtttl, fruitjam-services, fruitjam-buttons, fruitjam-usbhost, fruitjam-hidkeys, mosquitto_pub, mosquitto_sub, fruitjam-mem, free, fruitjam-uptime, uptime, fruitjam-du, du, fruitjam-ps, ps, fruitjam-pgrep, pgrep, pkill, wget, telnet, nc, tftp, and vi found in PATH. |
| Berry expression | PASS | berry -e 'print("berry-cdc-ok")' printed berry-cdc-ok. |
| Board status helper | PASS | fruitjamctl status reported LED, USB power, peripheral reset, and three buttons. |
| Button helper | PASS | fruitjam-buttons status reported button1/button2/button3 released. |
| I2C scan | PASS | fruitjam-i2c scan found device 0x18. |
| NeoPixel device | PASS | printf 'clear\nwrite\n' > /dev/neopixels completed. |
| AirLift probe | PASS | airliftctl probe reported spidev, firmware, MAC, status, and IP fields. |
| AirLift scan | PASS | airliftctl scan found visible networks. |
| AirLift WiFi join | PASS | Joined configured WiFi credentials and printed status/IP progress. |
| AirLift TCP GET | PASS | airliftctl tcp-get example.com / returned HTTP/1.1 200 OK. |
| Phase reboot | PASS | Test suite used fruitjamctl bootsel then picotool reboot -a -c riscv before service tests. |
| Service status | PASS | fruitjam-services status returned service/listener status without spawning large helpers. |
| HTTP CGI | PASS | AirLift HTTP returned mode":"airlift-direct" status JSON; loopback CGI is available through fruitjam-services core. |
| Telnet | PASS | AirLift telnet printed Fruit Jam telnet shell and echoed TELNET_SPEED_OK. |
| TFTP loopback | PASS | TFTP GET returned Fruit Jam TFTP area when the core loopback services were started. |
| FTP passive | PASS | Passive FTP over AirLift listed /mnt/sd, uploaded a file, and downloaded it with matching checksum. |
| RTTTL audio | PASS | scripts/fruitjam-audio-mic-test.py --serial-port /dev/cu.usbserial-P97cvdxp --mic-device 1 --loud --tone-hz 880 --tone-ms 2500 matched the 880 Hz tone from the speaker. |
These examples are meant to be run from the Fruit Jam console over USB CDC, UART, or telnet. They cover the features that have been brought up so far.
-
Check the default boot services and listeners:
fruitjam-services status
Expected services include the AirLift inbound worker and
fruitjam-buttons daemon. External HTTP, telnet, and FTP are served by the AirLift worker after WiFi joins. The AirLift heartbeat line should stay below its 60-second stale restart window while the inbound worker is healthy. -
Start the optional loopback service set for target-side HTTP/TFTP/FTP tests:
fruitjam-services core
This starts
fruitjam-httpd,fruitjam-telnetd,fruitjam-ftpd, and TFTP on the local Linux network stack. It is useful for smoke tests from the console; the default boot path keeps these larger daemons out of memory until requested. -
Fetch the SD-card placeholder page through the tiny HTTP client:
wget -O - http://127.0.0.1/index.html
-
Run the CGI environment smoke test:
wget -O - http://127.0.0.1/cgi-bin/env.cgi
Expected output starts with
Fruit Jam CGI OK. -
Open the hardware playground:
wget -O - http://127.0.0.1/playground wget -O - http://127.0.0.1/cgi-bin/fruitjam.cgi?action=statusIn a browser, visit
http://<board-ip>/playgroundafter AirLift joins WiFi. The page has status tiles, NeoPixel color pickers, button tests, RTTTL playback, WAV listing/playback, DVI helpers, USB-host status, Berry example controls, ADC reads, and I2C checks. -
Drive NeoPixels through HTTP CGI using direct
/dev/neopixelswrites:wget -O - 'http://127.0.0.1/cgi-bin/fruitjam.cgi?action=neopixels&c0=%23ff0000&c1=%23ffaa00&c2=%2300ff66&c3=%230088ff&c4=%23aa44ff'Expected JSON includes
"message":"neopixels updated". The web page no longer uses Berry for this fast hardware path; Berry is only invoked by the explicit Berry example runner. -
Play the default RTTTL tune through the web API:
wget -O - 'http://127.0.0.1/cgi-bin/fruitjam.cgi?action=rtttl' -
Scan I2C and read ADC through the web API:
wget -O - 'http://127.0.0.1/cgi-bin/fruitjam.cgi?action=i2c' wget -O - 'http://127.0.0.1/cgi-bin/fruitjam.cgi?action=adc&channel=temp'
-
Exercise the telnet shell over loopback:
(echo 'echo TELNET_OK'; echo status; echo exit) | telnet 127.0.0.1 23
Expected output includes
Fruit Jam telnet shellandTELNET_OK. Interactive telnet sessions also support up/down history recall and tab completion for builtins, commands in/bin,/usr/bin,/sbin, and/usr/sbin, plus filesystem paths in command arguments. -
Check the FTP control path:
(echo USER anonymous; echo PASS x; echo PWD; echo TYPE I; echo QUIT) | nc 127.0.0.1 21Expected output includes 220 Fruit Jam FTP ready, 230 Login ok, and
221 Bye.
- Fetch the TFTP README into
/tmp:
tftp -g -r README.txt -l /tmp/tftp-readme.txt 127.0.0.1
cat /tmp/tftp-readme.txtExpected output is Fruit Jam TFTP area.
- Use
ncas a tiny TCP server and client:
echo SERVERDATA > /tmp/nc-send.txt
: > /tmp/nc-recv.txt
nc -l -p 7008 < /tmp/nc-send.txt > /tmp/nc-recv.txt &
echo CLIENTDATA | nc 127.0.0.1 7008
cat /tmp/nc-recv.txtExpected client output is SERVERDATA; the final cat should show
CLIENTDATA.
- Bridge a serial device over TCP:
/root/sh/serial-over-tcp.sh 7000 /dev/ttyGS0In another shell, connect with nc 127.0.0.1 7000. With only the current
userspace NINA helper there is no Linux wlan0, so remote access to this port
needs a future Linux network interface or bridge.
- Toggle the GPIO29 LED through sysfs:
echo 29 > /sys/class/gpio/export 2>/dev/null || true
echo out > /sys/class/gpio/gpio29/direction
echo 1 > /sys/class/gpio/gpio29/value
cat /sys/class/gpio/gpio29/value
echo 0 > /sys/class/gpio/gpio29/value-
Toggle a female-header GPIO, for example GPIO6:
echo 6 > /sys/class/gpio/export 2>/dev/null || true echo out > /sys/class/gpio/gpio6/direction echo 1 > /sys/class/gpio/gpio6/value cat /sys/class/gpio/gpio6/value echo 0 > /sys/class/gpio/gpio6/value
-
Inspect the three onboard buttons:
fruitjam-buttons status
GPIO0 is button 1, GPIO4 is button 2, and GPIO5 is button 3.
-
Inject a synthetic button event and inspect the SQLite log:
echo 'button2 gpio4 test 123' > /run/fruitjam-buttons.fifo fruitjam-buttonlog dump /mnt/sd/fruitjam/buttons.db 5 tail -n 5 /mnt/sd/fruitjam/buttons.log
-
Write, unmount, remount, and read the SD card:
echo fruitjam_sd_audit > /mnt/sd/audit.txt umount /mnt/sd mount /mnt/sd cat /mnt/sd/audit.txt
-
Read ADC channels:
fruitjam-adc read 0 fruitjam-adc read temp
-
Scan or ping the I2C bus:
fruitjam-i2c scan fruitjam-i2c ping 0x18 berry-run /root/berry/11-i2c.be
The TLV320DAC3100 control interface should acknowledge at
0x18. -
Start the TLV320 clock path and run the RTTTL audio bring-up helper:
echo start > /dev/fruitjam-audio fruitjam-rtttl --loud --tone 880 2500 echo stop > /dev/fruitjam-audio
Audible output was verified from the host with
scripts/fruitjam-audio-mic-test.py. WAV playback exists throughfruitjam-wavplay, but SD-card WAV files should be treated separately from the audio path when the kernel reports FAT read errors.To install and verify the generated sample WAV at
/mnt/sd/wavs/fruitjam-scale.wav:scripts/fruitjam-audio-mic-test.py \ --serial-port /dev/cu.usbserial-P97cvdxp \ --install-sd-wav --run-wavplay --loud --rtttl scale
-
Run Berry expressions, scripts, and the REPL:
berry -e 'print("hello fruit jam")' berry -e 'import math print(math.sqrt(81))' berry-run /root/berry/12-usbhost-keyboard.be berry-run /root/berry/13-airlift.be berry-run /root/berry/14-audio-wav.be berry-run /root/berry/15-board-control.be berry-run /root/berry/neopixels.be berry
12-usbhost-keyboard.beprepares thekbd-find/live keyboard smoke commands for the PIO USB-host bridge, including the history/path-completion capablekbd-auto-shellhelper.berry-run /root/berry/neopixels.beshould drive the five onboard NeoPixels. -
Inspect AirLift firmware, join WiFi, and perform an HTTP fetch through the ESP32-C6:
airliftctl fw airliftctl join SSID PASSWORD airliftctl status airliftctl ip airliftctl tcp-get example.com /
For automatic AirLift service join on the board, put credentials on the SD card instead of in the rootfs overlay:
mkdir -p /mnt/sd/fruitjam chmod 700 /mnt/sd/fruitjam cat > /mnt/sd/fruitjam/wifi.conf <<'EOF' SSID=your-ssid PASSWORD=your-passphrase EOF chmod 600 /mnt/sd/fruitjam/wifi.conf
Do not put real WiFi credentials in commits, image overlays, or public logs.
-
Publish and subscribe to MQTT through AirLift:
mosquitto_sub --airlift -h 192.0.2.10 -p 1883 -t 'fruitjam/#' -C 1 -W 30 -v mosquitto_pub --airlift -h 192.0.2.10 -p 1883 -t fruitjam/test -m hello-from-fruitjam berry-run /root/berry/09-mqtt-publish.be berry-run /root/berry/10-mqtt-subscribe.beReplace the broker address with your own MQTT broker. Add
-u USER -P PASSWORDwhen your broker requires authentication. The Berry examples use thefruitjammodule MQTT helpers to build AirLift-awaremosquitto_pubandmosquitto_subcommands, then write environment-configurable scripts under/tmp.For an end-to-end publish and subscribe smoke test, run the focused host helper from the repo. It keeps the broker password out of command output:
export FJ_MQTT_HOST=192.0.2.10 export FJ_MQTT_USERNAME=your-user export FJ_MQTT_PASSWORD=your-password ./scripts/mqtt-smoke-test.py --transport cdc --port /dev/tty.usbmodem1101 ./scripts/mqtt-smoke-test.py --transport telnet --telnet-host <board-ip>
The smoke test proves both directions: target
mosquitto_pub --airliftreceived by a host-side subscriber, and targetmosquitto_sub --airliftreceiving a host-side publish. -
Route button events to MQTT through the SD-card config:
mkdir -p /mnt/sd/fruitjam printf '%s\n' \ MQTT_TRANSPORT=airlift \ MQTT_HOST=192.0.2.10 \ MQTT_PORT=1883 \ MQTT_USER= \ MQTT_PASSWORD= \ MQTT_TOPIC=fruitjam/buttons \ > /mnt/sd/fruitjam/buttons.conf fruitjam-services restart echo 'button2 gpio4 test 223' > /run/fruitjam-buttons.fifo
fruitjam-buttonsreads/etc/fruitjam-buttons.conffirst and/mnt/sd/fruitjam/buttons.confsecond at daemon startup, so restart services after changing the SD-card config. -
Re-enter software BOOTSEL without pressing the physical button:
fruitjamctl bootsel
This disconnects the running system and should make the RP2350 BOOTSEL volume appear on the host.
From the host, the repository helper can do the same recovery dance and then flash the current UF2 once
picotoolsees BOOTSEL. It tries the AirLift HTTP BOOTSEL action, the AirLift telnet shell, a DTR/RTS-disabled hardware UART shell if one is available, and CDC recovery. On macOS, CDC recovery tries both/dev/cu.usbmodem*and the matching/dev/tty.usbmodem*counterpart unless--no-tty-counterpartis passed:FJ_HTTP_HOST=<board-ip> FJ_TELNET_HOST=<board-ip> \ ./scripts/fruitjam-recover-flash.py -v
To force the hardware UART route, pass
--uart-port /dev/cu.usbserial-P97cvdxpor setFJ_UART_PORT=/dev/cu.usbserial-P97cvdxp.
Build and flash:
git submodule update --init
make -C buildroot BR2_EXTERNAL=$PWD adafruit_fruit_jam_rp2350_defconfig
make -C buildroot
picotool load -fu buildroot/output/images/flash-image.uf2If picotool was not available during the Buildroot post-image step, generate the
UF2 manually:
picotool uf2 convert buildroot/output/images/flash-image.bin \
buildroot/output/images/flash-image.uf2 --family rp2350-riscvSee board/adafruit/adafruit_fruit_jam_rp2350/README.md
for board assumptions, serial wiring, expected boot log, and limitations. See
docs/pinmap-fruitjam.md, docs/risks.md,
and docs/milestones.md for follow-on work.
The original target is still available:
git submodule update --init
make -C buildroot BR2_EXTERNAL=$PWD raspberrypi-pico2_defconfig
make -C buildroot
picotool load -fu buildroot/output/images/flash-image.uf2RP2350 reports a Store/AMO access fault if RISC-V AMO/exclusive operations target a region that does not support atomics. External PSRAM is not normal cache-coherent RAM, but this port places the kernel in PSRAM. The existing patch stack includes RP2350 workarounds for Linux atomics; do not remove those patches without a replacement and a hardware test plan.