Overview
The Pinephone is an open source smartphone that runs Linux. It's a really neat device and will hopefully be suitable as a daily driver smartphone in the near future, but for now it's very pleasant to hack on. Short surface level overview of the device here.
The below example is a little contrived, but hopefully sheds some light on connecting external peripherals to the Pinephone. I'm also fairly new to embedded stuff, D-bus, and still shaky on my Rust knowledge, so critique on a better way to implement all this is very welcome.
I²C
I²C is a common method for interfacing with embedded peripherals. It's a fairly simple communication method. For a deeper dive checkout out the Embedded Rust Discovery Book.
Embedded rust
In order to understand how the driver works we need to understand a little bit about the embedded rust ecosystem. embedded-hal
is a set of traits which can be used to create platform-agnostic drivers. The SSD1306
targets embedded-hal
, so any hal that implements the embedded-hal
traits will be able to use the driver. In order to use SSD1306
driver on our Pinephone we'll take advantage of the linux-embeddedhal
crate.
I2c on the pinephone
Accessing the pogo pins
You'll need some way to connect your device to the Pinephone. The easiest method is to just purchase a breakout board. But, there are other ways to access these pins. I used the breakout board from this repo and ordered them from OSH Park. There is some soldering required.
Connect the display
Connecting the display is pretty straightforward.
One you've connected your breakout you just need to match up the pins (GND
VCC
SCL
SDA
) to the display.
I²C on Linux
🚨 You may need to enable the proper kernel driver. I'm using Arch Linux on my pinephone, which appears to have the I²C kernel module already enabled.
Make sure you have have the i2cdetect
utility installed. On arch linux the utility is in the i2c-tools
package. With that tool installed run i2cdetect -l
to print out all the i2c devices:
i2c-3 unknown mv64xxx_i2c adapter N/A
i2c-1 unknown mv64xxx_i2c adapter N/A
i2c-4 unknown i2c-csi N/A
i2c-2 unknown mv64xxx_i2c adapter N/A
i2c-0 unknown DesignWare HDMI N/A
i2c-5 unknown i2c-2-mux (chan_id 0) N/A
Each I²C device will have an address we use to interface with it. The SSD1306
by default has an address of 3C
. In order to figure out which I²C device we need to communicate with, we can probe each of the interfaces. Here is my output after probing device 3
$ i2cdetect 3
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
/dev/i2c-3
appears to be the interface the SSD1306 display is connected to. With that knowledge we can start programming some Rust!
Rust!
Create a new Rust project. Use rustup if you don't already have it installed.
cargo new i2c-test && cd i2c-test
Modify Cargo.toml
to add the dependencies we need:
# ...Omitted
[dependencies]
ssd1306 = "0.5.2"
linux-embedded-hal = "0.3.0"
embedded-graphics = "0.6.2"
And then modify src/main.rs
use embedded_graphics::{
fonts::{Font6x8, Text},
pixelcolor::BinaryColor,
prelude::*,
style::TextStyleBuilder,
};
use ssd1306::{mode::GraphicsMode, prelude::\*, Builder, I2CDIBuilder};
use core::fmt::Write;
use linux_embedded_hal::I2cdev;
fn main() {
let i2c = I2cdev::new("/dev/i2c-3").unwrap(); // Replace with the proper interface!
let interface = I2CDIBuilder::new().init(i2c);
let mut disp: GraphicsMode<_, _> = Builder::new()
.size(DisplaySize128x64)
.connect(interface).into();
disp.init().unwrap();
let text_style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.build();
Text::new("Hello world!", Point::zero())
.intostyled(text_style)
.draw(&mut disp);
disp.flush().unwrap();
}
Before we run this code let's take a step back and look at the permissions on the i2c-#
devices.
$ ls -l /dev/ | grep i2c
crw------- 1 root root 89, 0 May 4 12:09 i2c-0
crw------- 1 root root 89, 1 May 4 12:09 i2c-1
crw------- 1 root root 89, 2 May 4 12:09 i2c-2
crw------- 1 root root 89, 3 May 4 12:09 i2c-3
crw------- 1 root root 89, 4 May 4 12:09 i2c-4
crw------- 1 root root 89, 5 May 4 12:09 i2c-5
These devices are both owned by root and in the group root. If we try to run our rust code right now, the code will fail because our user lacks permission to read from the I²C device. A quick fix is to run:
chown $(whoami) /dev/i2c-#
(replace #
with the I²C # you found from the previous section).
Now we can run: cargo run
and you should see the display light up with Hello world!
.
📝 There isn't anything Pinephone specific about this code. You should be able to run this exact program (provided that it references the right device) on something like a raspberry pi.
Let's do something more elaborate
Wouldn't it be cool, although maybe a little unnecessary if we could display the cell signal on our new OLED screen? Let's do it!
D-bus modem manager
D-bus is a messaging middleware for communicating between multiple processes. What we're interested in is the ModemManager
D-bus interface. Take a look at the modemmanager documenation to get an idea of what you can do with this interface. D-Feet is a great little app that can list all the different interfaces you can connect to and all the methods and properties they provide.
D-bus and Rust
Generating our D-bus interface
To actually interact with D-bus we'll generate the rust code necessary using dbus-codegen. This is surprisingly easy once you have all the pieces. First install the utility:
cargo install dbus-codegen
And then run the utility. My command ended up looking something like this:
dbus-codegen-rust -s -g -m None -d org.freedesktop.ModemManager1 -p /org/freedesktop/ModemManager1/Modem/0 > src/modemmanager.rs
Printing out the cell signal level
At this point your rust directory should look like this:
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
└── modemmanager.rs
First, let's update Cargo.toml
with the dbus
dependency
[dependencies]
dbus = "0.9.2"
# Extra deps omitted!
Now, we can get the signal level with the following code:
mod modemmanager;
//...
// Omitted extra packages!
//...
use dbus::{blocking::Connection, arg};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Notice that we used `new_system`
// This connects to the `system` dbus
let conn = Connection::new_system()?;
// Second, create a wrapper struct around the connection that makes it easy
// to send method calls to a specific destination and path.
let proxy = conn.with_proxy("org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1/Modem/0", Duration::from_millis(5000));
// Import our generated interface!
use modemmanager::OrgFreedesktopDBusProperties;
// Use d-feet to make sure that this is the same on your system.
// Look for the `SignalQuality` property
let signal_quality: Box<dyn arg::RefArg> = proxy.get("org.freedesktop.ModemManager1.Modem", "SignalQuality")?;
// Cast the signal quality to an i64, yes this has some code smell, but we're just hacking this :)
// in here
let signal: i64 = signal_quality
.as_iter()
.unwrap()
.next()
.unwrap()
.as_i64()
.unwrap();
// ...
// I2C code from the previous omitted
// ...
Ok(())
}
What next?
Look at the methods generated by dbus-codegen
. Instead of just running this method and closing we could listen for the PropertiesChanged
signal to update the display. Or look at the existing embedded hal drivers and pick up some new hardware to play with. Any of the sensors listed there should work just as easily as the SSD1306 display. If you have something which doesn't have a driver in Rust yet, well writing embedded drivers in rust isn't that hard. Give it a try.
The Pinephone may still be a little rough to use as a daily driver, but it has a lot of potential and hackability. I hope more people invest in this device and we start seeing some more interesting projects.
Top comments (0)