Making a Flipper App, Part 2: A CLI Command
Table of Contents
TL;DR
How to make a Flipper CLI command in an external app
Update: Since I wrote this, there’s been some new and exciting work on the CLI. If you’re using firmware newer than 1.3, take a look at subshells. This will also work, though.
- Make an app with a GUI that stays open until the back button is pressed. I copied the example_view_holder and added the CLI code around that.
- When the GUI app starts, register a CLI command callback/entry point by calling
cli_registry_add_command(formerly calledcli_add_command), and use the flagCliCommandFlagParallelSafeso it can run at the same time as the GUI part of the app. - Right before the app exits, remove the command by calling
cli_registry_delete_command(formerly calledcli_delete_command). If you don’t remove the CLI command and it’s invoked after the app is closed, it causes a null pointer dereference and the Flipper will crash. - To write to the CLI from the command line callback function, use
pipe_send(formerlycli_write) orprintf. I primarily usedprintf, but in one case I tried to useprintfand it sent the output to the log instead of the CLI, so that time I usedpipe_send. Do not use my project as an example of safely cleaning up and exiting an app while the CLI part of it is potentially running, because it does not do that (yet).This is pretty much fixed. The CLI command locks a lock while it’s running, and when the back button is pressed, the app waits for that to be released before it exits.
The long version
What does the app do?
It lets you use the Flipper with the tamagometer web app to record Tamagotchis visiting each other. The Flipper is more reliable than the thing I initially put together with the pico, and this will probably make people more likely to use the app.
If I stumbled across this project, being able to use a Flipper instead of ordering parts and assembling them would be the difference between actually trying it out, or scrolling by idly thinking, “Hmm, interesting.” Firmware that runs on a pico/other cheap hardware is still going to be maintained, since the whole point of adding Flipper support is wider accessibility, and making it a Flipper exclusive project would defeat the purpose.
Other things that didn’t quite fit my use case
Built in CLI
In the Flipper’s CLI mode, there is an infrared CLI app. It can wait for signals and print whatever it receives, and transmit signals. I hoped maybe I could just use that to print out the raw Tamagotchi signals, and I could move the decoding work into the front end.
Unfortunately, it could not reliably pick up back and forth conversations between Tamagotchis. The Flipper waits 150 milliseconds to detect the end of a signal and then process it, and the messages from a Tamagotchi conversation came in too quickly.
If this had worked and I’d used it, the front end would have needed a major overhaul to interpret the output from the IR CLI app. It might have been worth it anyway to make a separate app to replicate what I’ve been doing with the pico, so the front end doesn’t have to change (at least not much). As it was, the decision was very easy, since it just didn’t work.
RPC
The command start_rpc_session puts the Flipper into RPC mode, and then you can send protobufs to it to execute different commands. The extent of the official documentation about this is:
Activates the remote procedure call (RPC) session. Switches the CLI into protobuf mode. Normally, you don’t need to do that.
Hm. But what if I do?
It’s open source, so I just checked how it’s implemented. Here’s the part of the infrared app that responds to RPC commands and it only handles:
- Stopping the RPC session
- Closing the app
- Loading a file
- Simulating pressing and releasing the physical buttons on the Flipper
This seems to be mostly for the remote control feature in the mobile app.
As it happens, the docs were right, and I don’t need to do that. That’s honestly a relief, because RPC mode is not very well documented, and I don’t have much experience with RPC and protobuf. It would be a lot to take on all at once. This is still good to know about, though.
Okay, it’s got to be its own app
In broad strokes, the plan is…
To listen for an IR signal:
- Set a time out for 1 second that will close the app and print a timeout message if no signals are received
- Make an infrared worker
- Register a callback with the infrared worker to decode and print any signals it receives
- Use
furi_hal_infrared_async_rx_set_timeoutto set the IR signal detection timeout to something shorter than 150,000 microseconds (I’m thinking more like 10,000 or less should do it, the maximum gap I expect in the real message is about 6000 us) - Wait around for a signal or a timeout
To transmit:
- Encode the bits and send the result using
infrared_send_raw
CLI
The original Pico firmware has a very simple REPL, responding to listen by listening for 1 second for IR signals, and responding to send<command> by sending that command.
To do that on the Flipper, I can register a CLI command and then check for the arguments listen or send<command>. This will change the interface a tiny bit (I’ll have to send tamagometer listen from the web app instead of just listen) but that’s okay.
If I was really worried about maintaining the interface, I could just register listen and send as CLI commands, but there’s no need to clutter up the flipper’s CLI with really general commands.
Launching the app with loader
I didn’t end up doing this because it was relatively slow to load the app off of the SD card. Until I do a major overhaul,1 I’m polling repeatedly for a second at a time, so it needs to run as immediately as possible.
That said, here’s how to launch an app with loader it since it could definitely be useful in other situations:
loader open /ext/apps/<FolderName>/<FileName>.fap
To find the file path I used the Flipper mobile app’s file explorer, or you could plug the SD card into a computer.
loader (along with the rest of the CLI commands) seems to be used mostly in the build tools (like fbt launch), and automated tests.
Registering a CLI command
The built in IR CLI command uses the function cli_add_command. In fact, every built in app with a CLI component calls that function to register their CLI callbacks when the Flipper starts up. External apps don’t have the option to run something on startup (at least it wouldn’t let me use the app type “startup” in the manifest, which makes sense from a security standpoint I guess).
So, what if I just register the command the first time someone runs the app? Turns out if you add a CLI command, exit the program that added it, and then use the CLI app, you get a null pointer dereference. That also makes sense. The app is no longer loaded into memory so the pointer to the function is not pointing to anything any longer.2
(In real life this was the first thing I tried, and there was a multi-day pause here to construct a wifi dev board, read a ton of docs, and get VSCode and gdb set up well enough to actually figure out why the Flipper was crashing)
A search for cli_add_command (which is now called cli_registry_add_command) on discord turned up someone trying to do a similar thing in 2022.
Applications application is runy: is it possible to write an app in such a manner that (when running it?) you can extend cli functionality?
Dr_Zlo: Yeah. You will need to call
cli_add_commandon fap start andcli_delete_commandon exit.
The function names have since changed to cli_registry_add_command and cli_registry_delete_command but it works!
I copied the example_view_holder app out of the firmware repo since I just need some kind of GUI that stays open until the back button is pressed. I augmented that app so it adds a CLI command when it starts up, then deletes the CLI command on exit.
The command showed up in the CLI, yay! But when I ran it, I got an error that there’s a program running already. Boo. Way better than a segfault, but still not functional.
After some more trawling through the firmware, I found out passing the flag CliCommandFlagParallelSafe to cli_add_command instead of the CliCommandFlagDefault flag fixes that.3 We have liftoff!
Doing stuff in the CLI callback
I wanted to print some output when the CLI command gets called, and sometimes I saw the built in apps use cli_write, and sometimes they used printf. If I can get away with using printf, I’ll try that, because I already kind of know how to use it.
printf worked everywhere except in the IR signal received callback, where it sent the output into the logs instead of the CLI. I guess the fact that it’s being invoked as a callback from the IR worker does something to change stdout. pipe_write successfully wrote to the CLI, so in this case I just used that instead. The built in IR CLI app also uses pipe_write in its signal received callback, so it’s probably working as intended.
Receiving an IR signal
The built in IR apps and IR scope all use the infrared_worker, and register a callback with it to handle received signals. The code to interface more directly with the hardware looks complicated and I really want to minimize the amount of wheels I’m reinventing, so I also did that.
This means it’ll still be a little different from the code on the Pico, since it’s going to be looking for a gap to detect a signal has ended, instead of a long ending mark. I actually considered looking for a gap to separate signals when I was designing the Pico firmware, but the timeout logic seemed complicated to implement, and signal has a specific stop indicator, so I didn’t. This time the very generous Flipper firmware developers have already done it, and I get to take advantage of their hard work. Yay.
Encoding and Decoding
The Flipper does not have built in support for the Tamagotchi message format (darn), but it does offer several other more common and useful formats. Could I use some of the built in logic for detecting and decoding those so I’m not starting totally from scratch?
The built-in IR protocols
Upon inspection, the answer is no, not really. It seems hard to use the built in IR encoding/decoding machinery outside of the actual firmware. The thing to do, if I wanted to, would be to create my own InfraredDecoderHandler which only handles Tamagotchi messages. I’d replace my InfraredWorker’s default one with that. However, that’s made with a big static struct where all the decoders are registered, and I’d need to figure out how to replace that. (the situation may have changed, but it wasn’t a viable option in late 2024)
This would only be worth doing if I wanted to add this format to the firmware, and that doesn’t seem necessary or desirable. It would be kind of funny, but if I read the code correctly, it would also try to decode a Tamagotchi signal every time anyone received any infrared signal at all, which would be pretty wasteful.
Writing a custom encoder and decoder
In the original Pico design, I decoded the signal in place as it came in, and the code for that was not pretty.4 In this case, I’m getting a list of timings ready to go from the Flipper, so that’ll make life a bit easier.
To decode:
- Are there at least 323 timings? If not, it can’t be a complete message, return false.
- In the preamble, does the 0th element approximately equal 9500 and the 1st approximately equal 6000? If not, return false.
- In the main section, iterate through and interpret 160 pairs of timings:
- mark + short space => 0
- mark + long space => 1
- After 160 pairs there should be a long mark, and that’s the end
- If any of that didn’t match, it’s not a valid signal, and return false.
To encode:
- Add the preamble (9600, 6000) to the array of timings
- For each bit, if it’s 0 add a mark and a short space. If it’s 1 add a mark and a long space.
- Add the ending mark.
If I were hardcore and really cared about space efficiency I’d store the 1s and 0s as actual bits and do a bunch of bit twiddling, but I’m afraid I’m not and I don’t. 160 bytes is not that big, and it’s so much easier to iterate through the bits as chars. And they’re just getting printed back out over serial anyway.
Conclusion
Now that I have a mostly functioning app I can say that making it was fun! Definitely not a short or direct path. I’m glad I did the early prototyping in micro python on a pico, so I had a solid idea of what I was trying to implement on the Flipper, or it would have been a tougher road.
Maybe now I can finally study the Tamagotchi messages to figure out how to win all the games….
Links
-
And I am planning to, eventually. Maybe. ↩︎
-
There’s a post in the Discord that says plugins stay in RAM, so you can start one from an app, and then do things with it after the app closes. I didn’t look into this at all because it seemed like a lot of complication with little benefit for what I’m doing, but it could be really useful for some other scenario. ↩︎
-
This does raise the question, “is the CLI command I’ve registered actually parallel safe?”. Not really inherently, but in this context the GUI app is preventing anything else from running at the same time, and the GUI is doing nothing, so it’s not competing for access to resources. ↩︎
-
Writing it that way seemed like a necessary and good idea for some reason that I am presently forgetting. I think it was that I needed to detect and give up on decoding invalid messages without a timeout, which will no longer be necessary after the fabled rewrite. ↩︎