1
1
mirror of https://github.com/NixOS/mobile-nixos.git synced 2024-12-14 09:16:39 +03:00

examples/hello: introduce a minimal useful example system

The examples/hello system can be used by users that want to boot a
minimal, and cross-compilable system.

This is better than a "raw" build of the root of the Mobile NixOS repo
since it provides a stage-2 application stating the system booted
successfully.
This commit is contained in:
Samuel Dionne-Riel 2020-05-27 18:27:40 -04:00
parent b38caeec1a
commit 3d0552223d
11 changed files with 657 additions and 0 deletions

View File

@ -60,6 +60,8 @@ let
;
in
(
# Don't break if `device` is not set.
if device == null then (id: id) else
if device ? special
then header "Evaluating: ${device.name}"
else header "Evaluating device: ${device}"
@ -94,6 +96,13 @@ in
un-configured image. That image can be configured using `local.nix`.
**Note that an unconfigured image may appear to hang at boot.**
An alternative is to use one of the `examples` system. They differ in their
configuration. An example that should be building, and working using
cross-compilation is the `examples/hello` system. Read its README for more
information.
$ nix-build examples/hello --argstr device ${final_device} -A build.default
*************************************************************************
* Please also read your device's documentation for further usage notes. *
*************************************************************************

23
examples/hello/README.md Normal file
View File

@ -0,0 +1,23 @@
> **NOTE**: This example system can be used to make a minimal system that can
> be built using cross-compilation, to validate that the device goes to stage-2.
## Building
```
$ cd .../mobile-nixos
$ nix-build examples/hello --argstr device DEVICE-NAME -A build.default
```
## Installing
Follow the installation instructions for your device.
## Running
This system should boot using the usual stage-1 boot process, followed by a
specialized stage-2 configuration that runs a single-purpose application to
provide a tangible proof that the boot process has completed successfully.
Note that there is no expected way to use this system other than to see the
specialized application starting. This is not intended to be a starting point
to configure a "normal" system on your device.

View File

@ -0,0 +1,28 @@
{ stdenv
, lib
, mruby
}:
stdenv.mkDerivation {
name = "hello-gui.mrb";
src = lib.cleanSource ./.;
nativeBuildInputs = [
mruby
];
buildPhase = ''
mrbc -g -o app.mrb \
$(find ${../../../boot/gui/lib} -type f -name '*.rb' | sort) \
$(find ./lib -type f -name '*.rb' | sort) \
main.rb
'';
installPhase = ''
mkdir -p $out/libexec/
mv -v app.mrb $out/libexec/
mkdir -p $out/share/hello-gui
'';
}

View File

@ -0,0 +1,256 @@
module GUI
# Refreshing at 120 times per second *really* helps with the drag operations
# responsiveness. At 60 it feels a bit sluggish.
# This likely comes from the naïve implementation that we are not refreshing at
# 60 times per seconds, but rather, refresh and wait 1/60th of a second. This
# makes the refresh rate a tad slower.
# Boosting to 120 doesn't seem to have ill effects. It's simply refreshed more.
REFRESH_RATE = 120
# UI constants
NIXOS_LIGHT_HUE = 205
NIXOS_DARK_HUE = 220
DIR = File.dirname($0)
ASSETS_DIR = File.join(File.dirname($0), "../share/hello-gui")
# Sets things up; back box for some ugly hacks.
def self.init()
return if @initialized
@initialized = true
# Get exclusive control of the framebuffer
# By design we will not restore the console at exit.
# We are assuming the target does not necessarily have a console attached to
# the framebuffer, so this program has to be enough by itself.
VTConsole.map_console(0)
# Used by the "simulator"
LVGL::Hacks.monitor_width = 720
LVGL::Hacks.monitor_height = 1280
# Prepare LVGL
LVGL::Hacks.init()
# And switch to the desired theme
LVGL::Hacks.theme_night(NIXOS_LIGHT_HUE)
end
# Runs the app, black boxes LVGL things.
def self.main_loop()
# Main loop
while true
LVGL::Hacks::LVTask.handle_tasks
sleep(1.0/REFRESH_RATE)
end
end
# Wraps an LVGL widget.
class Widget
def initialize(widget)
@widget = widget
end
def method_missing(*args)
@widget.send(*args)
end
end
class Button < Widget
def initialize(parent)
super(LVGL::LVButton.new(parent))
set_layout(LVGL::LAYOUT::COL_M)
set_ink_in_time(200)
set_ink_wait_time(100)
set_ink_out_time(500)
set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
@label = LVGL::LVLabel.new(self)
end
def set_label(label)
@label.set_text(label)
end
end
# Common pattern for a "back button".
# Handles its presentation, and handles its behaviour.
class BackButton < Button
# +parent+: Parent object
# +location+: An instance on which `present` can be called.
def initialize(parent, location)
@holder = LVGL::LVContainer.new(parent)
@holder.set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
@holder.set_style(LVGL::CONT_STYLE::MAIN, LVGL::LVStyle::STYLE_TRANSP.dup)
style = @holder.get_style(LVGL::CONT_STYLE::MAIN)
style.body_padding_top = 0
style.body_padding_left = 0
style.body_padding_right = 0
style.body_padding_bottom = 0
super(@holder)
@location = location
set_label("Back")
set_fit2(LVGL::FIT::NONE, LVGL::FIT::TIGHT)
set_width(@holder.get_width / 2)
set_x(0)
self.event_handler = ->(event) do
case event
when LVGL::EVENT::CLICKED
location.present()
end
end
end
end
# Implements a clock as a wrapped LVLabel.
class Clock < Widget
def initialize(parent)
super(LVGL::LVLabel.new(parent))
set_align(LVGL::LABEL_ALIGN::LEFT)
set_long_mode(LVGL::LABEL_LONG::CROP)
# Update the text once
update_clock
# Then register a task to update regularly.
@task = LVGL::Hacks::LVTask.create_task(250, LVGL::TASK_PRIO::MID, ->() do
update_clock
end)
end
def update_clock()
now = Time.now
set_text([
:hour,
:min,
:sec,
].map{|fn| now.send(fn).to_s.rjust(2, "0") }.join(":"))
end
end
# Implements a battery widget as a wrapped LVLabel.
class Battery < Widget
def initialize(parent)
super(LVGL::LVLabel.new(parent))
set_align(LVGL::LABEL_ALIGN::RIGHT)
set_long_mode(LVGL::LABEL_LONG::CROP)
@battery = HAL::Battery.main_battery
# Update the text once
update_text
# Then register a task to update regularly.
@task = LVGL::Hacks::LVTask.create_task(1000 * 15, LVGL::TASK_PRIO::LOW, ->() do
update_text
end)
end
def update_text()
if @battery
set_text("(#{@battery.status}) #{@battery.percent}%")
else
set_text("[no known battery]")
end
end
end
# Empty invisible widget
class Screen < Widget
def initialize()
super(LVGL::LVContainer.new())
set_layout(LVGL::LAYOUT::COL_M)
style = get_style(LVGL::CONT_STYLE::MAIN).dup
set_style(LVGL::CONT_STYLE::MAIN, style)
style.body_padding_top = 0
style.body_padding_left = 0
style.body_padding_right = 0
style.body_padding_bottom = 0
style.body_padding_inner = 0
end
end
# Scrolling page.
class Page < Widget
def initialize(parent)
# A "holder" widget to work around idiosyncracies of pages.
@holder = LVGL::LVContainer.new(parent)
@holder.set_fit2(LVGL::FIT::FILL, LVGL::FIT::NONE)
@holder.set_style(LVGL::CONT_STYLE::MAIN, LVGL::LVStyle::STYLE_TRANSP.dup)
@holder.set_height(parent.get_height_fit - @holder.get_y)
# The actual widget we interact with
super(LVGL::LVPage.new(@holder))
style = LVGL::LVStyle::STYLE_TRANSP.dup
# Padding to zero in the actual scrolling widget makes the scrollbar visible
style.body_padding_top = style.body_padding_top / 2
style.body_padding_left = 0
style.body_padding_right = 0
set_style(LVGL::PAGE_STYLE::BG, style)
set_style(LVGL::PAGE_STYLE::SCRL, style)
set_fit2(LVGL::FIT::FILL, LVGL::FIT::NONE)
# Filling the parent that is at the root of the screen is apparently broken :/.
set_height(@holder.get_height - get_y)
# Make this scroll
set_scrl_layout(LVGL::LAYOUT::COL_M)
end
end
# Widget implementing the whole header
class Header < Widget
def initialize(parent)
super(LVGL::LVContainer.new(parent))
header_style = get_style(LVGL::CONT_STYLE::MAIN).dup
set_style(LVGL::CONT_STYLE::MAIN, header_style)
header_style.glass = 1
header_style.body_radius = 0
header_style.body_opa = 255 * 0.6
set_fit2(LVGL::FIT::FILL, LVGL::FIT::TIGHT)
set_layout(LVGL::LAYOUT::PRETTY)
# Split 50/50
child_width = (
get_width -
header_style.body_padding_left -
header_style.body_padding_right -
header_style.body_padding_inner*2
) / 2
# [00:00 ]
@clock = Clock.new(self)
@clock.set_width(child_width)
# [ 69%]
@battery = Battery.new(self)
@battery.set_width(child_width)
end
end
# Extend this to make a "window"
class BaseWindow
include Singleton
def initialize()
super()
# Initializes GUI things if required...
GUI.init
# Preps a basic display
@screen = Screen.new()
@header = Header.new(@screen)
@container = Page.new(@screen)
end
# Switch to this window
def present()
LVGL::FFI.lv_disp_load_scr(@screen.lv_obj_pointer)
end
end
end

View File

@ -0,0 +1,76 @@
module HAL
class Battery
NODE_BASE = "/sys/class/power_supply"
# Guesstimates the main battery for a list of likely candidates
def self.main_battery()
node = %w{
battery
bms
BAT0
}.map { |name| File.join(NODE_BASE, name) }
.find { |path| File.exist?(path) }
if node
Battery.new(node)
else
nil
end
end
def initialize(node)
@node = node
end
# google-walleye
# [nixos@nixos:/sys/class/power_supply]$ cat battery/uevent
# POWER_SUPPLY_NAME=battery
# POWER_SUPPLY_INPUT_SUSPEND=0
# POWER_SUPPLY_STATUS=Charging
# POWER_SUPPLY_HEALTH=Good
# POWER_SUPPLY_PRESENT=1
# POWER_SUPPLY_CHARGE_TYPE=Fast
# POWER_SUPPLY_CAPACITY=56
# POWER_SUPPLY_SYSTEM_TEMP_LEVEL=0
# POWER_SUPPLY_CHARGER_TEMP=364
# POWER_SUPPLY_CHARGER_TEMP_MAX=803
# POWER_SUPPLY_INPUT_CURRENT_LIMITED=1
# POWER_SUPPLY_VOLTAGE_NOW=3780507
# POWER_SUPPLY_VOLTAGE_MAX=4400000
# POWER_SUPPLY_VOLTAGE_QNOVO=-22
# POWER_SUPPLY_CURRENT_NOW=183105
# POWER_SUPPLY_CURRENT_QNOVO=-22
# POWER_SUPPLY_CONSTANT_CHARGE_CURRENT_MAX=2700000
# POWER_SUPPLY_TEMP=320
# POWER_SUPPLY_TECHNOLOGY=Li-ion
# POWER_SUPPLY_STEP_CHARGING_ENABLED=0
# POWER_SUPPLY_STEP_CHARGING_STEP=-1
# POWER_SUPPLY_CHARGE_DISABLE=0
# POWER_SUPPLY_CHARGE_DONE=0
# POWER_SUPPLY_PARALLEL_DISABLE=0
# POWER_SUPPLY_SET_SHIP_MODE=0
# POWER_SUPPLY_CHARGE_FULL=2805000
# POWER_SUPPLY_DIE_HEALTH=Cool
# POWER_SUPPLY_RERUN_AICL=0
# POWER_SUPPLY_DP_DM=0
# POWER_SUPPLY_CHARGE_COUNTER=1455951
# POWER_SUPPLY_CYCLE_COUNT=849
def uevent()
File.read(File.join(@node, "uevent")).split("\n").map do |line|
key, value = line.split("=", 2)
[key.downcase.to_sym, value]
end.to_h
end
def name()
uevent[:power_supply_name] || "unknown"
end
def status()
uevent[:power_supply_status] || "unknown"
end
def percent()
uevent[:power_supply_capacity] || "unknown"
end
end
end

View File

@ -0,0 +1,91 @@
module GUI
# Helper methods to help creating a "button palette" kind of window.
module ButtonPalette
def add_button(label)
Button.new(@container).tap do |btn|
btn.glue_obj(true)
btn.set_label(label)
btn.event_handler = ->(event) do
case event
when LVGL::EVENT::CLICKED
yield
end
end
end
end
def add_buttons(list)
list.each do |pair|
label, action = pair
add_button(label, &action)
end
end
end
class MainWindow < BaseWindow
include ButtonPalette
def initialize()
super()
LVGL::LVLabel.new(@container).tap do |label|
label.set_long_mode(LVGL::LABEL_LONG::BREAK)
label.set_text(%Q{\nSelect from the following options})
label.set_align(LVGL::LABEL_ALIGN::CENTER)
label.set_width(@container.get_width_fit)
end
add_buttons([
["About", ->() { AboutWindow.instance.present }],
["Quit", ->() { QuitWindow.instance.present }],
])
end
end
class QuitWindow < BaseWindow
include ButtonPalette
def run(*cmd)
$stderr.puts " $ " + cmd.join(" ")
system(*cmd) unless LVGL::Introspection.simulator?
end
def initialize()
super()
BackButton.new(@container, MainWindow.instance)
add_buttons([
["Reboot", ->() { run("reboot") }],
["Reboot to recovery", ->() { run("reboot recovery") }],
["Reboot to bootloader", ->() { run("reboot bootloader") }],
["Power off", ->() { run("poweroff") }],
])
if LVGL::Introspection.simulator?
add_button("Quit") { exit(0) }
end
end
end
class AboutWindow < BaseWindow
include ButtonPalette
def initialize()
super()
BackButton.new(@container, MainWindow.instance)
LVGL::LVLabel.new(@container).tap do |label|
text = <<EOF
Mobile NixOS "Hello GUI"
This application is intended to provide a minimum viable known working framebuffer application to test different components of your mobile device.
This is NOT a complete useful system.
EOF
label.set_long_mode(LVGL::LABEL_LONG::BREAK)
label.set_text(%Q{\n#{text}})
label.set_align(LVGL::LABEL_ALIGN::CENTER)
label.set_width(@container.get_width_fit)
end
end
end
end

View File

@ -0,0 +1,3 @@
GUI::MainWindow.instance.present
GUI.main_loop

View File

@ -0,0 +1,26 @@
{ stdenv
, lib
, callPackage
, mrbgems
, mruby
, mobile-nixos
}:
let
script-loader = mobile-nixos.stage-1.script-loader.override({
mrbgems = mrbgems // {
mruby-lvgui = callPackage ../../../overlay/mruby-builder/mrbgems/mruby-lvgui {
withSimulator = true;
};
};
});
applet = callPackage ./. {};
in
(script-loader.wrap {
name = "simulator";
applet = "${applet}/libexec/app.mrb";
}).overrideAttrs(old: rec {
pname = "hello-gui-simulator";
version = "0.0.1";
name = "${pname}-${version}";
})

View File

@ -0,0 +1,106 @@
{ config, lib, pkgs, ... }:
let
inherit (lib.strings) makeBinPath;
hello-gui = pkgs.mobile-nixos.stage-1.script-loader.wrap {
name = "hello-gui";
applet = "${pkgs.callPackage ./app {}}/libexec/app.mrb";
env = {
PATH = "${makeBinPath (with pkgs;[
systemd # journalctl
glibc # iconv
utillinux # lsblk
input-utils # lsinput
])}:$PATH";
};
};
tmpfsConf = {
device = "tmpfs";
fsType = "tmpfs";
neededForBoot = true;
};
in
{
imports = [
./workaround-v4l_id-hang.nix
];
environment.systemPackages = with pkgs; [
hello-gui
input-utils
];
# Make the system rootfs different enough that mixing stage-1 and stage-2
# will fail and not have weird unexpected behaviours.
mobile.generatedFilesystems = {
rootfs = lib.mkDefault {
label = lib.mkForce "MOBILE_HELLO";
id = lib.mkForce "12345678-1324-1234-0000-D00D00000001";
};
};
fileSystems = {
"/" = lib.mkDefault {
autoResize = lib.mkForce false;
};
# Nothing is saved, except for the nix store being rehydrated.
"/tmp" = tmpfsConf;
"/var/log" = tmpfsConf;
"/home" = tmpfsConf;
};
mobile.boot.stage-1.bootConfig = {
# This will be useful for debugging boot issues over serial in the default
# configuration.
log.level = "DEBUG";
};
systemd.services.hello-gui = {
description = "GUI for the hello example of Mobile NixOS";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
SyslogIdentifier = "hello-gui";
ExecStart = ''
${hello-gui}/bin/hello-gui
'';
};
};
# Only enable `adb` if we know how to.
# FIXME: relies on implementation details. Poor separation of concerns.
mobile.adbd.enable = (config.mobile.system.type == "android") &&
(config.mobile.usb.mode != "gadgetfs" || config.mobile.usb.gadgetfs.functions ? ffs)
;
boot.postBootCommands = lib.mkOrder (-1) ''
brightness=10
echo "Setting brightness to $brightness"
if [ -e /sys/class/backlight/backlight/brightness ]; then
echo $(($(cat /sys/class/backlight/backlight/max_brightness) * brightness / 100)) > /sys/class/backlight/backlight/brightness
elif [ -e /sys/class/leds/lcd-backlight/max_brightness ]; then
echo $(($(cat /sys/class/leds/lcd-backlight/max_brightness) * brightness / 100)) > /sys/class/leds/lcd-backlight/brightness
elif [ -e /sys/class/leds/lcd-backlight/brightness ]; then
# Assumes max brightness is 100... probably wrong, but good enough, eh.
echo $brightness > /sys/class/leds/lcd-backlight/brightness
fi
'';
users.users.nixos = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" ];
};
security.sudo = {
enable = true;
wheelNeedsPassword = lib.mkForce false;
};
services.mingetty.autologinUser = "nixos";
system.build = {
app-simulator = pkgs.callPackage ./app/simulator.nix {};
};
}

View File

@ -0,0 +1,22 @@
{ device ? null }:
let
system-build = import ../../. {
inherit device;
configuration = [ { imports = [ ./configuration.nix ]; } ];
};
in
system-build // {
___readme-default = throw ''
Cannot directly build for ${device}...
You can build the `-A build.default` attribute to build the default output
for your device.
$ nix-build examples/hello --argstr device ${device} -A build.default
*************************************************************************
* Please also read your device's documentation for further usage notes. *
*************************************************************************
'';
}

View File

@ -0,0 +1,17 @@
# This works around an issue on at least one device (motorola-addison) where
# the v4l_id tool from udev hangs for more than a minute on boot.
#
# This replaces the file from udev with an empty one.
{ pkgs, lib, ... }:
let
emptyV4lRules = pkgs.runCommandNoCC "empty-v4l-rules" {} ''
mkdir -p $out/lib/udev/rules.d
touch $out/lib/udev/rules.d/60-persistent-v4l.rules
'';
in
{
services.udev.packages = lib.mkOrder 10000 [
emptyV4lRules
];
}