Skip to content

Instantly share code, notes, and snippets.

@nmattia
Created November 12, 2025 21:17
Show Gist options
  • Select an option

  • Save nmattia/f785d3325f98fa373cb067b5d4ed2576 to your computer and use it in GitHub Desktop.

Select an option

Save nmattia/f785d3325f98fa373cb067b5d4ed2576 to your computer and use it in GitHub Desktop.
Rust snake game for charlieplexed display on flipper zero

charlieplexed LED snake

Based on https://github.com/flipperzero-rs/flipperzero-template

Build

cargo build --release && cp ./target/thumbv7em-none-eabihf/release/flipperzero-charlieplex.fap .

then drag/drop onto FlipperZero

update the icon:

(echo -ne '\x00'; convert ./src/snake-10x10.png mono:-) > ./src/snake-10x10.icon

TODO:

cargo install --locked flipperzero-tools --target aarch64-apple-darwin
cargo run --bin <bin name>
cp ./target/thumbv7em-none-eabihf/release/<bin name> ./<bin name>.fap
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# when calling `cargo run`, install & run the fap
runner = "run-fap"
[target.thumbv7em-none-eabihf]
rustflags = [
# CPU is Cortex-M4 (STM32WB55)
"-C", "target-cpu=cortex-m4",
# Size optimizations
"-C", "panic=abort",
"-C", "debuginfo=0",
"-C", "opt-level=z",
# LTO helps reduce binary size
"-C", "embed-bitcode=yes",
"-C", "lto=yes",
# Linker flags for relocatable binary
"-C", "link-args=--script=flipperzero-rt.ld --Bstatic --relocatable --discard-all --strip-all --lto-O3 --lto-whole-program-visibility",
]
[build]
target = "thumbv7em-none-eabihf"
[package]
name = "flipperzero-charlieplex"
version = "0.0.1"
edition = "2021"
[dependencies]
flipperzero = { version = "0.15.0", features = ["alloc"] }
flipperzero-sys = { version = "0.15.0" }
flipperzero-rt = { version = "0.15.0" }
rand_core = "0.9.3"
flipperzero-alloc = "0.15.0"
//! My custom panel
#![no_main]
#![no_std]
// Required for panic handler
extern crate flipperzero_rt;
// Required for panic handler
extern crate flipperzero_alloc;
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use core::ffi::CStr;
use core::ffi::c_void;
// GUI record
const RECORD_GUI: &CStr = c"gui";
const FULLSCREEN: sys::GuiLayer = sys::GuiLayerFullscreen;
use flipperzero;
use flipperzero::furi::message_queue::MessageQueue;
use flipperzero_rt::{entry, manifest};
use flipperzero_sys as sys;
enum SnakeEvent {
EventKey(sys::InputEvent),
}
type EventQueue = MessageQueue<SnakeEvent>;
// Define the FAP Manifest for this application
manifest!(
name = "Charlie the Snake",
app_version = 1,
has_icon = true,
// See https://github.com/flipperzero-rs/flipperzero/blob/v0.11.0/docs/icons.md for icon format
icon = "../snake-10x10.icon",
);
// Define the entry function
entry!(main);
/// Input event handler.
unsafe extern "C" fn input_callback(event: *mut sys::InputEvent, context: *mut c_void) {
assert!(!context.is_null());
let q: &mut EventQueue = unsafe { &mut *(context as *mut EventQueue) };
let snake_event = SnakeEvent::EventKey(*event);
// will block (this thread) until queue frees up
let _ = q.put(
snake_event,
flipperzero::furi::time::FuriDuration::WAIT_FOREVER,
);
}
#[derive(Clone, Copy)]
struct State {
high: sys::GpioPin,
low: sys::GpioPin,
}
unsafe fn led_pins() -> [sys::GpioPin; 6] {
[
sys::gpio_ext_pa7,
sys::gpio_ext_pa6,
sys::gpio_ext_pa4,
sys::gpio_ext_pb3,
sys::gpio_ext_pb2,
sys::gpio_ext_pc3,
]
}
unsafe fn all_off() {
for pin in led_pins() {
sys::furi_hal_gpio_init(
&pin,
sys::GpioModeInput,
sys::GpioPullNo,
sys::GpioSpeedVeryHigh,
);
}
}
fn lookup_pins(x: u8, y: u8) -> (u8, u8) {
let a7 = 0;
let a6 = 1;
let a4 = 2;
let b3 = 3;
let b2 = 4;
let c3 = 5;
match (x, y) {
// First double column
(0, 0) => (a7, a6),
(1, 0) => (a6, a7),
(0, 1) => (a6, a4),
(1, 1) => (a4, a6),
(0, 2) => (a4, b3),
(1, 2) => (b3, a4),
(0, 3) => (b3, b2),
(1, 3) => (b2, b3),
(0, 4) => (b2, c3),
(1, 4) => (c3, b2),
// Second double column
(2, 0) => (a7, a4),
(3, 0) => (a4, a7),
(2, 1) => (a4, b2),
(3, 1) => (b2, a4),
(2, 2) => (b2, a7),
(3, 2) => (a7, b2),
(2, 3) => (a7, b3),
(3, 3) => (b3, a7),
(2, 4) => (b3, c3),
(3, 4) => (c3, b3),
// Third double column
(4, 0) => (b3, a6),
(5, 0) => (a6, b3),
(4, 1) => (a6, b2),
(5, 1) => (b2, a6),
(4, 2) => (b2, a7),
(5, 2) => (a7, b2),
(4, 3) => (a7, c3),
(5, 3) => (c3, a7),
(4, 4) => (c3, a4),
(5, 4) => (a4, c3),
_ => (0, 1),
}
}
#[derive(Copy, Clone)]
struct Panel {
last: Option<State>,
}
impl Panel {
unsafe fn set(self: &mut Self, p: Option<State>) {
if let Some(last) = self.last {
sys::furi_hal_gpio_init(
&last.high,
sys::GpioModeInput,
sys::GpioPullNo,
sys::GpioSpeedVeryHigh,
);
sys::furi_hal_gpio_init(
&last.low,
sys::GpioModeInput,
sys::GpioPullNo,
sys::GpioSpeedVeryHigh,
);
}
if let Some(p) = p {
sys::furi_hal_gpio_init(
&p.high,
sys::GpioModeOutputPushPull,
sys::GpioPullNo,
sys::GpioSpeedVeryHigh,
);
sys::furi_hal_gpio_write(&p.high, true);
sys::furi_hal_gpio_init(
&p.low,
sys::GpioModeOutputPushPull,
sys::GpioPullNo,
sys::GpioSpeedVeryHigh,
);
sys::furi_hal_gpio_write(&p.low, false);
}
self.last = p;
}
}
enum Direction {
DOWN,
UP,
LEFT,
RIGHT,
}
fn step(snake: &mut Vec<(u8, u8)>, direction: &Direction) {
let mut iter = snake.iter_mut().peekable();
while let Some(part) = iter.next() {
if let Some(nxt) = iter.peek() {
part.0 = nxt.0;
part.1 = nxt.1;
} else {
match direction {
Direction::UP => {
part.1 = part.1 - 1;
}
Direction::DOWN => {
part.1 = part.1 + 1;
}
Direction::LEFT => {
part.0 = part.0 - 1;
}
Direction::RIGHT => {
part.0 = part.0 + 1;
}
}
}
}
}
struct DisplayState {
what: DisplayWhat,
n_ticks: usize,
}
enum DisplayWhat {
Snake { part_n: usize },
}
impl DisplayState {
fn tick(self: &mut Self, snake: &Vec<(u8, u8)>) {
if self.n_ticks > 0 {
self.n_ticks -= 1;
return;
}
let (what, n_ticks) = match self.what {
DisplayWhat::Snake { part_n } => {
let part_n = (part_n + 1) % snake.len();
let what = DisplayWhat::Snake { part_n };
let n_ticks = part_n * part_n;
(what, n_ticks)
}
};
self.what = what;
self.n_ticks = n_ticks;
}
}
fn main(_args: Option<&CStr>) -> i32 {
unsafe {
// Alloc some datastructures
let view_port = sys::view_port_alloc();
let gui = sys::furi_record_open(RECORD_GUI.as_ptr()) as *mut sys::Gui;
sys::gui_add_view_port(gui, view_port, FULLSCREEN);
let mut event_queue: EventQueue = MessageQueue::new(100);
let event_queue_p: *mut EventQueue = &mut event_queue;
let event_queue_ptr: *mut c_void = event_queue_p.cast();
sys::view_port_input_callback_set(view_port, Some(input_callback), event_queue_ptr);
let mut panel = Panel { last: None };
// Turn everything off
all_off();
let mut i = 0;
let pins = led_pins();
let mut snake: Vec<(u8, u8)> = vec![(1, 0), (1, 1), (1, 2), (1, 3)];
let mut display_state = DisplayState {
what: DisplayWhat::Snake { part_n: 0 },
n_ticks: 0,
};
let mut direction = Direction::DOWN;
'processing: loop {
i += 1;
if i % 500 == 0 {
step(&mut snake, &direction);
}
display_state.tick(&snake);
let (c, r) = match display_state.what {
DisplayWhat::Snake { part_n } => snake[part_n],
};
let (h, l) = lookup_pins(c, r);
let pin_h = pins[h as usize];
let pin_l = pins[l as usize];
let state = State {
high: pin_h,
low: pin_l,
};
panel.set(Some(state));
if let Ok(snake_event) =
event_queue.get(flipperzero::furi::time::FuriDuration::from_millis(1))
{
match snake_event {
SnakeEvent::EventKey(key) => {
if key.type_ != sys::InputTypePress {
continue;
}
if key.key == sys::InputKeyBack {
break 'processing;
} else if key.key == sys::InputKeyDown {
direction = Direction::DOWN;
} else if key.key == sys::InputKeyUp {
direction = Direction::UP;
} else if key.key == sys::InputKeyLeft {
direction = Direction::LEFT;
} else if key.key == sys::InputKeyRight {
direction = Direction::RIGHT;
}
}
}
}
}
panel.set(None); // shut down any potential remaining pins
sys::view_port_enabled_set(view_port, false);
sys::gui_remove_view_port(gui, view_port);
sys::furi_record_close(RECORD_GUI.as_ptr());
sys::view_port_free(view_port);
}
0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment