-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Wrapper over notification API #34
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @Strosel,
I saw you opened this draft PR and added some initial feedback. Please let me know if you'd rather me wait until you've marked it as a non-draft.
Thanks for taking this on!
Thanks for all the feedback! I've tried to update my implementation using your notes, but there's still two roadblocks. The first is the default messages and sequences, I cant decide between redefining and re-exporting them which is safer but more time-consuming and harder to maintain or implement some kind of The second is the lifetimes of our sequences. With the current implementation the notification example mostly works but will either stop in the middle of the success sequence or crash altogether. It seems like this is because our rust app exits and releases the memory containing the sequence before the firmware has finished working through it. I don't know if this is solvable without leaking memory or using |
For some of the sequences (e.g.
I ran into some issues with this when writing the first version of the // This is OK
let sequence = &sys::sequence_set_only_red_255;
sys::notification_message(notification_app.as_ptr(), sequence);
// This is NOT
let sequence = sys::sequence_set_only_red_255;
sys::notification_message(notification_app.as_ptr(), &sequence); I'm not exactly sure how Rust handles slices defined within functions (e.g. If that doesn't work, there might be a fundamental limitation with using sequences defined in an external applications. Hopefully this is not the case. |
It would seem that the issue of non-blocking notifications cannot be solved with Aside from this, I cannot decide on how to layout the
#[repr(C)]
pub struct NotificationSequence<const N: usize>([&'static NotificationMessage; N], *const ()); the only drawback to this would be the const generic being somewhat restrictive, and will represent a false size (at least until generic_const_exprs is stable). However it could be made generic for other Null terminated arrays if needed in the future. This seems to be the last blocker so when this is resolved I will mark this PR as non-draft. |
It's possible to have pointers in static mut NOTIFICATION: [*const sys::NotificationMessage; 10] = unsafe { [
&sys::message_note_c4 as *const sys::NotificationMessage,
&sys::message_delay_100 as *const sys::NotificationMessage,
&sys::message_note_e4 as *const sys::NotificationMessage,
&sys::message_delay_100 as *const sys::NotificationMessage,
&sys::message_note_g4 as *const sys::NotificationMessage,
&sys::message_delay_100 as *const sys::NotificationMessage,
&sys::message_note_c5 as *const sys::NotificationMessage,
&sys::message_delay_100 as *const sys::NotificationMessage,
&sys::message_sound_off as *const sys::NotificationMessage,
ptr::null(),
] };
fn main(_args: *mut u8) -> i32 {
unsafe {
let notification_app = unsafe { sys::furi_record_open(RECORD_NOTIFICATION) } as *mut sys::NotificationApp;
sys::notification_message(notification_app, &NOTIFICATION as *const *const sys::NotificationMessage as *const sys::NotificationSequence);
sys::furi_record_close(RECORD_NOTIFICATION);
}
0
} This seems to be a little more reliable, but it's still possible to trigger a bus-fault or null-pointer deference after several attempts. I also was able to reproduce this in a C-based external application. const NotificationSequence chime = {
&message_note_c4,
&message_delay_100,
&message_note_e4,
&message_delay_100,
&message_note_g4,
&message_delay_100,
&message_note_c5,
&message_delay_100,
&message_sound_off,
NULL
};
int hello_world(char* /* arg */) {
NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION);
notification_message(notification_app, &chime);
furi_record_close(RECORD_NOTIFICATION);
return 0;
} So this is probably a firmware bug/race-condition. I'll open an issue on
I do something like this for padding of manifest fields, but that's a little easier since the size of the resulting array is known ahead of time. But I think your approach with a macro is fine. It's not uncommon to hide
Sounds good to me. I'm excited to have a safe high-level interface for notifications. |
Opened flipperdevices/flipperzero-firmware#2313 for the crash when using a custom |
It's surprisingly relieving that this is a bug in the firmware and not a rust issue 😅 The use of |
@dcoles This is ready for review |
Thanks. I should be able to take a look shortly (been a little sick this week). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some thoughts about the Rust-level API, which we can make more expressive than what the Flipper Zero SDK exposes.
@@ -0,0 +1,61 @@ | |||
use super::{Light, NotificationMessage}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this module is making certain methods more convenient to use (e.g. NotificationMessage::display_backlight_enforce_auto
), but it is also providing some common defaults (e.g. BLINK_START_100
) for which a user may have a good reason to call the base method for. It might be beneficial to separate these usages (because the constants that are equivalent to const fn
s add duplication to the crate API, unless the const fn
s get made pub(crate)
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The choice of re-exporting the results of the NotificationMessage
constructors with no inputs was mainly made to keep parity with the c API. However, I believe that this also increases the user experience somewhat as it enables the user to use a single export for all default messages if they only need and/or want those but I do see your point. What do you think of making the const
s that depend on parameters associated constants and scrapping the ones with no parameter to maybe get the best of both worlds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a desire to keep or provide parity with the C API, that might be better served with a prelude-style module designed for use flipperzero::notification::c_prelude::*;
usage, that collects / renames constants from other parts of the API. This would be a fine place for the constants that are duplicative of const fn
s to live.
As far as designing a "native" Rust API, grouping based on semantics rather than structure feels like it gives a more intuitive API (like my suggestion further down about module renaming). In an ideal world, the user wouldn't need to care much about whether a provided default corresponds to a single NotificationMessage
or multiple; they could just compose the messages as they need. We don't live in an ideal world, but macros let us get close.
What do you think of making the consts that depend on parameters associated constants
Associated constants of which types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point but as discussed with play_sound!
splatting sequences into each other is very difficult, especially doing so for arbitrary sequences due to slice length etc. therefore I would've preferred to keep messages and sequences separate but since notification_sequence!
is type checked I guess it would not be that much of an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that for the flipperzero
crate, there is no need for 1:1 party with the C API (some things might be unsafe or have a more idiomatic way to do in Rust). But the intent is to expose as much of features as (safely) possible.
It might even make sense to eventually have an alternate API that's a little more friendly than this one (e.g. Display::set_backlight
). NotificationMessage
lists aren't exactly the nicest things to work with.
@@ -0,0 +1,120 @@ | |||
use super::NotificationMessage; | |||
|
|||
pub static CLICK: NotificationMessage = NotificationMessage::sound_on(1.0, 1.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constant on its own doesn't produce a click; it is necessary to combine this with a delay and sound_off
. It might be more useful to provide this as a notification sequence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constant also exists simply for parity like the ones discussed above
|
||
pub static CLICK: NotificationMessage = NotificationMessage::sound_on(1.0, 1.0); | ||
|
||
pub const C0: NotificationMessage = NotificationMessage::sound_on(16.35, 1.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These all hard-code a volume of 1.0
, and AFAICT there is no way to alter this without calling NotificationMessage::sound_on
yourself with the same frequency constant (which is opaque).
Instead, we could define these constants using the following API:
/// A pitch at which Flipper Zero can play a sound.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pitch {
frequency: f32,
}
impl Pitch {
/// Defines a new pitch with the given frequency in Hertz.
///
/// # Safety
///
/// `frequency` must be positive and greater than zero.
pub const fn new(frequency: f32) -> Self {
Self { frequency }
}
/// Generates a notification message to start playing sound at this pitch and the
/// given volume.
///
/// The sound will continue until either another sound replaces it, or [`SOUND_OFF`]
/// is used.
///
/// # Safety
///
/// `volume` must be between `0.0` and `1.0`.
pub const fn play(self, volume: f32) -> NotificationMessage {
NotificationMessage::sound_on(self.frequency, volume)
}
}
pub const C0: Pitch = Pitch::new(16.35);
and then use it like:
pub const SUCCESS: NotificationSequence = notification_sequence![
messages::DISPLAY_BACKLIGHT_ON,
messages::GREEN_255,
messages::VIBRO_ON,
notes::C5.play(1.0),
messages::DELAY_50,
messages::VIBRO_OFF,
notes::E5.play(1.0),
messages::DELAY_50,
notes::G5.play(1.0),
messages::DELAY_50,
notes::C6.play(1.0),
messages::DELAY_50,
messages::SOUND_OFF,
];
I also experimented locally with an additional API:
/// A sound that Flipper Zero can play.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Sound {
pitch: Pitch,
duration: u32,
}
impl Sound {
/// Defines a new sound with the given pitch, and duration (in milliseconds).
pub const fn new(pitch: Pitch, duration: u32) -> Self {
Self { pitch, duration }
}
/// Generates a sequence of notification messages that will play this sound once at
/// the given volume.
///
/// # Panics
///
/// Panics if `volume < 0.0` or `volume > 1.0`.
pub const fn play_once(self, volume: f32) -> SingleSound {
(
self.pitch.play(volume),
NotificationMessage::delay(self.duration),
NotificationMessage::sound_off(),
)
}
}
// Only defined to make the tuple more ergonomic.
pub type SingleSound = (NotificationMessage, NotificationMessage, NotificationMessage);
const C5_50: SingleSound = Sound::new(notes::C5, 50).play_once(1.0);
const E5_50: SingleSound = Sound::new(notes::E5, 50).play_once(1.0);
const G5_50: SingleSound = Sound::new(notes::G5, 50).play_once(1.0);
const C6_50: SingleSound = Sound::new(notes::C6, 50).play_once(1.0);
pub const SUCCESS: NotificationSequence = notification_sequence![
messages::DISPLAY_BACKLIGHT_ON,
messages::GREEN_255,
messages::VIBRO_ON,
C5_50.0, C5_50.1,
messages::VIBRO_OFF,
E5_50.0, E5_50.1,
G5_50.0, G5_50.1,
C6_50.0, C6_50.1,
messages::SOUND_OFF,
];
It should be possible to rework the notification_sequence!
macro to make this ergonomic:
impl Sound {
/// Generates a sequence of notification messages that will play this sound for at
/// least its specified duration at the given volume.
///
/// The sound will continue after its specified duration until either another sound
/// replaces it, or [`SOUND_OFF`] is used.
///
/// # Panics
///
/// Panics if `volume < 0.0` or `volume > 1.0`.
pub const fn play(self, volume: f32) -> (NotificationMessage, NotificationMessage) {
(
self.pitch.play(volume),
NotificationMessage::delay(self.duration),
)
}
}
/// TODO: Change notification_sequence! to support flattening nested tuples or something.
const C5_50: Sound = Sound::new(notes::C5, 50);
const E5_50: Sound = Sound::new(notes::E5, 50);
const G5_50: Sound = Sound::new(notes::G5, 50);
const C6_50: Sound = Sound::new(notes::C6, 50);
pub const SUCCESS: NotificationSequence = notification_sequence![
messages::DISPLAY_BACKLIGHT_ON,
messages::GREEN_255,
messages::VIBRO_ON,
play_sound!(C5_50, 1.0),
messages::VIBRO_OFF,
play_sound!(E5_50, 1.0),
play_sound!(G5_50, 1.0),
play_sound!(C6_50, 1.0),
messages::SOUND_OFF,
];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great idea, I was thinking of something similar myself as I have written a toy music crate previously. IMHO the last example seems the best, but I would prefer if the duration and volume were swapped as the volume feels more intrinsic to the note than its duration. Another option would be to give the play_sound
macro optional volume and duration arguments with default values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After playing with this for a while I seem to be stuck on the play_sound
macro as it can't "splat" the tuple of messages in a way that makes them usable for notification_sequence!
any ideas?
What might be best for now is to keep the explicit API of "sound, delay, new sound/sound off" but add a convenience function for generating sound_on
messages with the desired pitch and volume
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if the duration and volume were swapped as the volume feels more intrinsic to the note than its duration
I had the reverse feeling, but not strongly; the thing I mainly wanted was the existence of Pitch
to allow reusing them. Given that NotificationMessage
groups them as ((frequency, volume), duration)
it also likely makes the macros easier to group them that way.
After playing with this for a while I seem to be stuck on the
play_sound
macro as it can't "splat" the tuple of messages in a way that makes them usable fornotification_sequence!
any ideas?
Yeah, I encountered the same issue in my brief period of playing with the API (and so I left it as experimental). I think it might be possible by making play_sound
not be a separate macro, but instead a keyword inside the notification_sequence!
macro (and then using recursion to parse the macro's arguments left-to-right, and splat the play_sound
tuple when we reach it).
What might be best for now is to keep the explicit API of "sound, delay, new sound/sound off" but add a convenience function for generating
sound_on
messages with the desired pitch and volume
Sounds good 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More disappointing news 😕 due to rust-lang/rust#57241 we can't calculate the frequency from some theoretical Pitch
enum so I think even a convenience function is impossible at this time. It could be nice to come back to when the issue is resolved though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, I was already in that issue as a result of this PR, asking if we can have a little const PartialEq
, as a treat.
Do we need to calculate frequencies though? I think the existing constant-based approach is fine, just using Pitch
constants instead of NotificationMessage
constants. If we also want to provide constants for pitch+volume like the C API does, that could be done. But even with an enum approach for a canonical set of sorted notes, the frequencies for them could just be constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please clarify how we are to use Pitch
constants instead of NotificationMessages
when we can not convert one into the other due to restrictions on floats in const?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pitch::play(self, volume)
is precisely this conversion. I don't understand what float calculations you are seeing as necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there may have been a misunderstanding on my part. I thought the examples you gave above were more separate and interpreted Pitch
in the later ones as something like
enum Pitch {
C,
Cs,
D,
...
}
which would need float calculations to generate a frequency.
While implementing Pitch
as only a frequency wrapper like above would be possible (and nice) I think the name could be changed to prevent this confusion for other users. Maybe something like this could be implemented instead
#[derive(Debug, Clone, Copy)]
pub struct Sound {
frequency: f32,
volume: f32,
duration: u32,
}
impl Sound {
pub const fn new(frequency: f32) -> Self {
Self {
frequency,
volume: 1.0,
duration: 0,
}
}
pub const fn volume(mut self, volume: f32) -> Self {
self.volume = volume;
self
}
pub const fn duration(mut self, duration: u32) -> Self {
self.duration = duration;
self
}
pub const fn play_once(self) -> NotificationMessage {
NotificationMessage::sound_on(self.frequency, self.volume)
}
pub const fn play(self) -> [NotificationMessage; 3] {
[
self.play_once(),
NotificationMessage::delay(self.duration),
NotificationMessage::sound_off(),
]
}
}
then a constructor using a Pitch
enum like above could be implemented in the future and the more challenging part right now would "only" be splatting
///Default notification notes. | ||
pub mod notes; | ||
///Default notification sequences. | ||
pub mod sequences; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than grouping all sequences together, the public API might be simpler if we group by kind of notification:
pub mod sequences; | |
pub mod backlight; | |
pub mod led; | |
pub mod sounds; | |
pub mod vibro; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a great idea to simplify the API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per my above reply, I think moving some of the single-message constants currently defined in messages
into these "kind-of" modules would also simplify the API. (Rust will already type-check to prevent users from mixing single-message and sequence types where we don't want them to.)
@@ -0,0 +1,61 @@ | |||
use super::{Light, NotificationMessage}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that for the flipperzero
crate, there is no need for 1:1 party with the C API (some things might be unsafe or have a more idiomatic way to do in Rust). But the intent is to expose as much of features as (safely) possible.
It might even make sense to eventually have an alternate API that's a little more friendly than this one (e.g. Display::set_backlight
). NotificationMessage
lists aren't exactly the nicest things to work with.
/// #Safety | ||
/// Due to how rust interacts with the firmware this function is not safe to use at any time | ||
/// where the application might exit directly afterwards as the rust runtime will free the | ||
/// sequence before the firmware has finished reading it. At any time where this is an issue | ||
/// `notify_blocking` should be used instead.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good news! This was resolved in flipperdevices/flipperzero-firmware#2335.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Do you think this may allow us to use dynamically allocated sequences? I would love to test myself but I'm honestly not sure on how to sync this branch with main
, I've only ever done so in my own repos where I'm fine with "ugly" merge commits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we'd still need to ensure that the dynamically allocated sequence wasn't free'd before the sequence had finished playing. The fix in flipperdevices/flipperzero-firmware#2335 fixes the case where the sequence is defined statically. What would happen was the program would be unloaded from memory, but the sequence still had a pointer to somewhere in the now free'd memory region. The fix just makes sure the Flipper runtime waits until all previous sequences have finished playing before unloading the program from memory.
The problem for a dynamically allocated sequences is that all this happens after main has exited:
// main
{
// create dynamically allocated sequence
let s = vec![...];
play(s); // non-blocking
// s is dropped, causing sequence to be freed
}
// oops! The flipper runtime wasn't finished with that sequence!
// CRASH!
wait_for_all_sequences_to_finish();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#49 will result in apps waiting for un-joined threads to finish before exiting the FAP, so I think a similar wait here would be fine (assuming the SDK provides a way for us to determine when it has finished).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the eventual answer here will need to be that play(s)
takes dynamic sequences by value, and under the hood stores them in an app-static reference (using e.g. #64) to ensure their lifetime extends beyond the user's main()
. Then we use the same waiting technique that the firmware does, but inside entry!()
instead (like #49 does for threads).
Hi @Strosel, Have you had any chance to address the comments on the PR? Would love to be able to merge it soon. |
Sadly I haven't hade time to look at this since March has been a lot busier than expected but hopefully I can look at it in the coming week. |
I think, this is ready now. The only thin missing I can think of is a mechanism to "splat" sequences into each other (eg. sounds) but I feel like this could be a feature added later. When/If such a feature is added we could also try to reorganize all messages into their appropriate sequence modules and remove some duplicates that would no longer be needed. |
This PR needs a rebase to address conflicts with recent changes in |
Everything should be rebased properly now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed as of 80e0ede. I checked that all the constants matched the ones defined in the Flipper Zero SDK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lightly tested ACK. Nice work!
Description
A WIP to support notifications without using sys in the user crate
Status
Currently this is a proof of concept for a notification wrapper, however the API needs some polish which has proven difficult due to the types involved. Currently sending a notification requires a
[&'static NotificationMessage; N]
which is converted into asys::NotificationSequence
via a somewhat messy process includingBox::leak
. Trying something as simple as wrapping the array in a struct seems to entirely break this process.There also seems to be a bug (probably due to some internal concurrency or the like) where running the notification example without a terminating
thread::sleep
causes the flipper to crash.