za/ch

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.

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:

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:

To transmit:

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_command on fap start and cli_delete_command on 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:

To encode:

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


  1. And I am planning to, eventually. Maybe. ↩︎

  2. 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. ↩︎

  3. 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. ↩︎

  4. 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. ↩︎

Tags: