Skip to content

Commit

Permalink
Add embedded-graphics driver
Browse files Browse the repository at this point in the history
This allows using `Canvas` as an `embedded-graphics` draw target.
It requires the `embedded-graphics` feature which is enabled by default.

Also included is an `embedded-graphics` example based on the
embedded-graphics "Hello world" example.
  • Loading branch information
dcoles committed Feb 14, 2025
1 parent 389ede0 commit 23058fd
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- `flipperzero::furi::hal::power::Power` handle to Power service
- High-level serial API (#216)
- Add `gui::Gui` record and basic support for using `gui::Canvas` in monopoly mode
- Add [`embedded-graphics`](https://crates.io/crates/embedded-graphics) support allowing `gui::Canvas`
to be used as a [`DrawTarget`](https://docs.rs/embedded-graphics/latest/embedded_graphics/draw_target/trait.DrawTarget.html) (#214)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion crates/build-examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
PYTHON = 'python'
TOOLS_PATH = '../tools'
INSTALL_PATH = PurePosixPath('/ext/apps/Examples')
ALL_EXAMPLES = {"dialog", "example_images", "gpio", "gui", "hello-rust", "notification", "serial-echo", "storage"}
ALL_EXAMPLES = {"dialog", "embedded-graphics", "example_images", "gpio", "gui", "hello-rust", "notification", "serial-echo", "storage"}


def parse_args():
Expand Down
17 changes: 16 additions & 1 deletion crates/flipperzero/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ bitflags = "2.4"

# Embedded-hal
embedded-hal = { version = "1.0.0-rc.1", optional = true }
embedded-hal-0 = { package = "embedded-hal", version = "0.2.7", features = ["unproven"], optional = true }
embedded-hal-0 = { package = "embedded-hal", version = "0.2.7", features = [
"unproven",
], optional = true }

# Embedded-graphics support
embedded-graphics-core = { version = "0.4.0", optional = true }

# Docs
document-features = { workspace = true, optional = true }

[dev-dependencies]
flipperzero-alloc.workspace = true
flipperzero-rt.workspace = true
embedded-graphics = "0.8.0"

# Toolbox
crc32fast = { version = "1", default-features = false }
Expand All @@ -55,6 +61,8 @@ crc32fast = { version = "1", default-features = false }

[features]

default = ["embedded-graphics"]

#! ## Core features

## Enables features requiring an allocator.
Expand All @@ -68,6 +76,9 @@ crc32fast = { version = "1", default-features = false }
## ```
alloc = []

## Enable embedded-graphics driver
embedded-graphics = ["dep:embedded-graphics-core"]

[lints.rust]
rust_2024_compatibility = "warn"
edition_2024_expr_fragment_specifier = "allow"
Expand All @@ -91,3 +102,7 @@ required-features = ["alloc"]
[[example]]
name = "threads"
required-features = ["alloc"]

[[example]]
name = "embedded-graphics"
required-features = ["embedded-graphics"]
105 changes: 105 additions & 0 deletions crates/flipperzero/examples/embedded-graphics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! embedded-graphics example for Flipper Zero.
//! This is based off the embedded-graphics "hello-world" example.
#![no_main]
#![no_std]

// Required for panic handler
extern crate flipperzero_rt;

// embedded-graphics requires a global allocator.
extern crate flipperzero_alloc;

use core::ffi::CStr;
use core::time::Duration;

use flipperzero::furi::thread::sleep;
use flipperzero::gui::Gui;
use flipperzero_rt::{entry, manifest};

use embedded_graphics::mono_font::{ascii::FONT_6X10, MonoTextStyle};
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{
Circle, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, StrokeAlignment, Triangle,
};
use embedded_graphics::text::{Alignment, Text};

// Define the FAP Manifest for this application
manifest!(
name = "Embedded Graphics",
app_version = 1,
has_icon = true,
// See `docs/icons.md` for icon format
icon = "icons/rustacean-10x10.icon",
);

// Define the entry function
entry!(main);

// Entry point
fn main(_args: Option<&CStr>) -> i32 {
let gui = Gui::open();
let mut canvas = gui.direct_draw_acquire();

// Create styles used by the drawing operations.
let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1);
let thick_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
let border_stroke = PrimitiveStyleBuilder::new()
.stroke_color(BinaryColor::On)
.stroke_width(3)
.stroke_alignment(StrokeAlignment::Inside)
.build();
let fill = PrimitiveStyle::with_fill(BinaryColor::On);
let character_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);

let yoffset = 14;

// Draw a 3px wide outline around the display.
canvas
.bounding_box()
.into_styled(border_stroke)
.draw(&mut *canvas)
.unwrap();

// Draw a triangle.
Triangle::new(
Point::new(16, 16 + yoffset),
Point::new(16 + 16, 16 + yoffset),
Point::new(16 + 8, yoffset),
)
.into_styled(thin_stroke)
.draw(&mut *canvas)
.unwrap();

// Draw a filled square
Rectangle::new(Point::new(52, yoffset), Size::new(16, 16))
.into_styled(fill)
.draw(&mut *canvas)
.unwrap();

// Draw a circle with a 3px wide stroke.
Circle::new(Point::new(88, yoffset), 17)
.into_styled(thick_stroke)
.draw(&mut *canvas)
.unwrap();

// Draw centered text.
let text = "embedded-graphics";
Text::with_alignment(
text,
canvas.bounding_box().center() + Point::new(0, 15),
character_style,
Alignment::Center,
)
.draw(&mut *canvas)
.unwrap();

// You must commit the canvas for it to display on screen.
canvas.commit();

// Show for a few seconds, then exit.
sleep(Duration::from_secs(5));

0
}
2 changes: 1 addition & 1 deletion crates/flipperzero/src/furi/event_flag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl EventFlag {
/// Get pointer to raw [`sys::FuriEventFlag`].
///
/// This pointer must not be `free`d or otherwise invalidated.
/// It must not be referenced after [`FuriEventFlag`] has been dropped.
/// It must not be referenced after [`EventFlag`] has been dropped.
pub fn as_ptr(&self) -> *mut sys::FuriEventFlag {
self.raw.as_ptr()
}
Expand Down
132 changes: 132 additions & 0 deletions crates/flipperzero/src/gui/canvas.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Canvases.
use core::cell::UnsafeCell;
use core::marker::PhantomPinned;

use flipperzero_sys as sys;

#[derive(Debug, Clone, Copy)]
Expand All @@ -22,3 +25,132 @@ impl Align {
}
}
}

/// Graphics Canvas.
#[repr(transparent)]
pub struct Canvas {
raw: UnsafeCell<sys::Canvas>,
_marker: PhantomPinned,
}

impl Canvas {
/// Get Canvas reference from raw pointer.
///
/// # Safety
/// Pointer must be non-null and point to a valid `sys::Canvas`.
/// This pointer must outlive this reference.
pub unsafe fn from_raw<'a>(raw: *mut sys::Canvas) -> &'a Self {
unsafe { &*(raw.cast()) }
}

/// Get Canvas reference from raw pointer.
///
/// # Safety
/// Pointer must be non-null and point to a valid `sys::Canvas`.
/// This pointer must outlive this reference.
pub unsafe fn from_raw_mut<'a>(raw: *mut sys::Canvas) -> &'a mut Self {
unsafe { &mut *(raw.cast()) }
}

/// Get pointer to raw [`sys::Canvas`].
pub fn as_ptr(&self) -> *mut sys::Canvas {
self.raw.get()
}

/// Get Canvas width and height.
pub fn get_size(&self) -> (usize, usize) {
unsafe {
(
sys::canvas_width(self.as_ptr()),
sys::canvas_height(self.as_ptr()),
)
}
}

/// Clear Canvas.
pub fn clear(&self) {
unsafe { sys::canvas_clear(self.as_ptr()) }
}

/// Commit Canvas and send buffer to display.
pub fn commit(&self) {
unsafe { sys::canvas_commit(self.as_ptr()) }
}
}

/// Support for [`embedded-graphics``](https://crates.io/crates/embedded-graphics) crate.
#[cfg(feature = "embedded-graphics")]
mod embedded_graphics {
use super::*;
use embedded_graphics_core::pixelcolor::BinaryColor;
use embedded_graphics_core::prelude::*;
use embedded_graphics_core::primitives::Rectangle;

impl Dimensions for Canvas {
fn bounding_box(&self) -> Rectangle {
let (width, height) = self.get_size();

Rectangle {
top_left: (0, 0).into(),
size: (width as u32, height as u32).into(),
}
}
}

impl DrawTarget for Canvas {
type Color = BinaryColor;
type Error = core::convert::Infallible;

fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let (width, height) = self.get_size();
let (width, height) = (width as i32, height as i32);

unsafe {
for Pixel(Point { x, y }, color) in pixels.into_iter() {
if (0..=width).contains(&x) && (0..=height).contains(&y) {
sys::canvas_set_color(self.as_ptr(), map_color(color));
sys::canvas_draw_dot(self.as_ptr(), x, y);
}
}
}

Ok(())
}

fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
// Clamp rectangle coordinates to visible display area
let area = area.intersection(&self.bounding_box());

// Do not draw if the intersection size is zero.
if area.bottom_right().is_none() {
return Ok(());
}

unsafe {
sys::canvas_set_color(self.as_ptr(), map_color(color));
sys::canvas_draw_box(
self.as_ptr(),
area.top_left.x,
area.top_left.y,
area.size.width as usize,
area.size.height as usize,
);
}

Ok(())
}
}

/// Map embedded-graphics color to Furi color.
#[inline]
const fn map_color(color: BinaryColor) -> sys::Color {
if color.is_on() {
sys::ColorBlack
} else {
sys::ColorWhite
}
}
}
Loading

0 comments on commit 23058fd

Please sign in to comment.