From 89bb4723646f7bdad952fa4d7d4e954161492203 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sat, 7 Mar 2026 23:50:35 +0100 Subject: [PATCH 01/11] cellular: First version, shows operator name and registration state --- .../META-INF/MANIFEST.JSON | 24 ++++ .../apps/cz.ucw.pavel.cellular/assets/main.py | 103 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 6765 bytes 3 files changed, 127 insertions(+) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py create mode 100644 internal_filesystem/apps/cz.ucw.pavel.cellular/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..9e6b0742 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "Xxx", +"publisher": "Pavel Machek", +"short_description": "Xxx", +"long_description": "Simple xxx app.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/icons/cz.ucw.pavel.xxx_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/mpks/cz.ucw.pavel.xxx_0.0.1.mpk", +"fullname": "cz.ucw.pavel.xxx", +"version": "0.0.1", +"category": "utilities", +"activities": [ + { + "entrypoint": "assets/main.py", + "classname": "Main", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py new file mode 100644 index 00000000..dcd5dcdd --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -0,0 +1,103 @@ +from mpos import Activity + +""" + +""" + +import time +import os +import json + +try: + import lvgl as lv +except ImportError: + pass + +from mpos import Activity, MposKeyboard + +TMP = "/tmp/cmd.json" + + +def run_cmd_json(cmd): + rc = os.system(cmd + " > " + TMP) + if rc != 0: + raise RuntimeError("command failed") + + with open(TMP, "r") as f: + data = f.read().strip() + + return json.loads(data) + +def dbus_json(cmd): + return run_cmd_json("sudo /home/mobian/g/MicroPythonOS/phone.py " + cmd) + +class CellularManager: + def init(self): + v = dbus_json("loc_on") + + def poll(self): + v = dbus_json("signal") + print(v) + self.signal = v + +cm = CellularManager() + +# ------------------------------------------------------------ +# +# ------------------------------------------------------------ + +class Main(Activity): + + def __init__(self): + super().__init__() + + # -------------------- + + def onCreate(self): + self.screen = lv.obj() + #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + + # Top labels + self.lbl_time = lv.label(self.screen) + self.lbl_time.set_style_text_font(lv.font_montserrat_24, 0) + self.lbl_time.set_text("Startup...") + self.lbl_time.align(lv.ALIGN.TOP_LEFT, 6, 22) + + self.lbl_date = lv.label(self.screen) + self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 48) + + self.lbl_month = lv.label(self.screen) + self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) + + self.setContentView(self.screen) + cm.init() + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 3000, None) + self.tick(0) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # -------------------- + + def tick(self, t): + now = time.localtime() + y, m, d = now[0], now[1], now[2] + hh, mm, ss = now[3], now[4], now[5] + + cm.poll() + s = "\n" + s += cm.signal["OperatorName"] + "\n" + s += "RegistrationState %d\n" % cm.signal["RegistrationState"] + s += "State %d\n" % cm.signal["State"] + + self.lbl_time.set_text("%02d:%02d" % (hh, mm)) + self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, s)) + + + # -------------------- + + diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.cellular/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..316c5f2366bdcab259ac9fb75ce7c3db1dfc4c65 GIT binary patch literal 6765 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 Date: Sat, 7 Mar 2026 23:55:14 +0100 Subject: [PATCH 02/11] cellular: bigger fonts to make it readable --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index dcd5dcdd..66e4b382 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -59,14 +59,16 @@ class Main(Activity): # Top labels self.lbl_time = lv.label(self.screen) - self.lbl_time.set_style_text_font(lv.font_montserrat_24, 0) + self.lbl_time.set_style_text_font(lv.font_montserrat_34, 0) self.lbl_time.set_text("Startup...") self.lbl_time.align(lv.ALIGN.TOP_LEFT, 6, 22) self.lbl_date = lv.label(self.screen) - self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 48) + self.lbl_date.set_style_text_font(lv.font_montserrat_20, 0) + self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 58) self.lbl_month = lv.label(self.screen) + self.lbl_month.set_style_text_font(lv.font_montserrat_20, 0) self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) self.setContentView(self.screen) From 952396ce4dae67f411e1455f0bb3c1f62e73416f Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 00:02:23 +0100 Subject: [PATCH 03/11] cellular: Move signal display away from date --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 66e4b382..9db2fdca 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -69,7 +69,7 @@ class Main(Activity): self.lbl_month = lv.label(self.screen) self.lbl_month.set_style_text_font(lv.font_montserrat_20, 0) - self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) + self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 22) self.setContentView(self.screen) cm.init() @@ -95,11 +95,17 @@ class Main(Activity): s += cm.signal["OperatorName"] + "\n" s += "RegistrationState %d\n" % cm.signal["RegistrationState"] s += "State %d\n" % cm.signal["State"] + sq, re = cm.signal["SignalQuality"] + s += "Signal %d\n" % sq + self.lbl_month.set_text(s) self.lbl_time.set_text("%02d:%02d" % (hh, mm)) + s = "" self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, s)) + + # -------------------- From ed337ef48c5643b7c670c2dee21fc945fc5f1c0b Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 21:22:51 +0100 Subject: [PATCH 04/11] cellular: Add input for phone number --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 9db2fdca..01480fac 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -65,17 +65,26 @@ class Main(Activity): self.lbl_date = lv.label(self.screen) self.lbl_date.set_style_text_font(lv.font_montserrat_20, 0) - self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 58) + self.lbl_date.align_to(self.lbl_time, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) + self.lbl_date.set_text("(details here?") self.lbl_month = lv.label(self.screen) self.lbl_month.set_style_text_font(lv.font_montserrat_20, 0) self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 22) + self.number = lv.textarea(self.screen) + self.number.set_accepted_chars("0123456789") + self.number.set_one_line(True) + self.number.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 12) + + kb = lv.keyboard(self.screen) + kb.set_textarea(self.number) + self.setContentView(self.screen) cm.init() def onResume(self, screen): - self.timer = lv.timer_create(self.tick, 3000, None) + self.timer = lv.timer_create(self.tick, 60000, None) self.tick(0) def onPause(self, screen): @@ -90,11 +99,13 @@ class Main(Activity): y, m, d = now[0], now[1], now[2] hh, mm, ss = now[3], now[4], now[5] + self.lbl_month.set_text("busy") + cm.poll() - s = "\n" + s = "" s += cm.signal["OperatorName"] + "\n" s += "RegistrationState %d\n" % cm.signal["RegistrationState"] - s += "State %d\n" % cm.signal["State"] + s += "State %d " % cm.signal["State"] sq, re = cm.signal["SignalQuality"] s += "Signal %d\n" % sq @@ -102,8 +113,6 @@ class Main(Activity): self.lbl_time.set_text("%02d:%02d" % (hh, mm)) s = "" self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, s)) - - # -------------------- From d8fe3e1271dbeb5d22dea4621a6a3e2b6d9cdf18 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 22:02:47 +0100 Subject: [PATCH 05/11] cellular: add number input / call button --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 01480fac..78d15054 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -73,12 +73,22 @@ class Main(Activity): self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 22) self.number = lv.textarea(self.screen) - self.number.set_accepted_chars("0123456789") + #self.number.set_accepted_chars("0123456789") self.number.set_one_line(True) + self.number.set_style_text_font(lv.font_montserrat_34, 0) self.number.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 12) + self.call = lv.button(self.screen) + self.call.align_to(self.number, lv.ALIGN.OUT_RIGHT_MID, 10, 0) + + l = lv.label(self.call) + l.set_text("Call") + l.center() + + kb = lv.keyboard(self.screen) kb.set_textarea(self.number) + kb.set_size(lv.pct(100), lv.pct(33)) self.setContentView(self.screen) cm.init() From 3c11fd60ce096f30af567994ef8aee64332bdcc4 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 22:12:31 +0100 Subject: [PATCH 06/11] cellular: hook up calls --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 78d15054..588349f9 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -40,6 +40,12 @@ class CellularManager: print(v) self.signal = v + def call(self, num): + v = dbus_json("call '%s'" % num) + + def sms(self, num, text): + v = dbus_json("call '%s' '%s'" % (num, text)) + cm = CellularManager() # ------------------------------------------------------------ @@ -80,7 +86,12 @@ class Main(Activity): self.call = lv.button(self.screen) self.call.align_to(self.number, lv.ALIGN.OUT_RIGHT_MID, 10, 0) + self.call.add_event_cb(lambda e: self.on_call(), lv.EVENT.CLICKED, None) + self.sms = lv.textarea(self.screen) + self.sms.set_style_text_font(lv.font_montserrat_24, 0) + self.sms.align_to(self.number, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + l = lv.label(self.call) l.set_text("Call") l.center() @@ -92,7 +103,7 @@ class Main(Activity): self.setContentView(self.screen) cm.init() - + def onResume(self, screen): self.timer = lv.timer_create(self.tick, 60000, None) self.tick(0) @@ -104,6 +115,15 @@ class Main(Activity): # -------------------- + def on_call(self): + num = self.number.get_text() + cm.call(num) + + def on_sms(self): + num = self.number.get_text() + text = self.sms.get_text() + cm.sms(num, text) + def tick(self, t): now = time.localtime() y, m, d = now[0], now[1], now[2] From 51fba25e4b3789130f829655853cd26a50f13de9 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 22:29:33 +0100 Subject: [PATCH 07/11] cellular: two textareas don't work well --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 588349f9..8e756cb5 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -88,15 +88,16 @@ class Main(Activity): self.call.align_to(self.number, lv.ALIGN.OUT_RIGHT_MID, 10, 0) self.call.add_event_cb(lambda e: self.on_call(), lv.EVENT.CLICKED, None) - self.sms = lv.textarea(self.screen) - self.sms.set_style_text_font(lv.font_montserrat_24, 0) - self.sms.align_to(self.number, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + # Two text areas on single screen don't work well. + # Perhaps make it dialog? + #self.sms = lv.textarea(self.screen) + #self.sms.set_style_text_font(lv.font_montserrat_24, 0) + #self.sms.align_to(self.number, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) l = lv.label(self.call) l.set_text("Call") l.center() - kb = lv.keyboard(self.screen) kb.set_textarea(self.number) kb.set_size(lv.pct(100), lv.pct(33)) From c02c75e8a856a6150210a5c53da1912a71d44fd7 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 8 Mar 2026 23:20:47 +0100 Subject: [PATCH 08/11] cellular: move phone.py helper to cellular --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 2 +- .../cz.ucw.pavel.cellular/assets/phone.py | 472 ++++++++++++++++++ 2 files changed, 473 insertions(+), 1 deletion(-) create mode 100755 internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index 8e756cb5..ddd1ebfd 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -29,7 +29,7 @@ def run_cmd_json(cmd): return json.loads(data) def dbus_json(cmd): - return run_cmd_json("sudo /home/mobian/g/MicroPythonOS/phone.py " + cmd) + return run_cmd_json("sudo /home/mobian/g/MicroPythonOS/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py " + cmd) class CellularManager: def init(self): diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py new file mode 100755 index 00000000..41dc7813 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +from pydbus import SystemBus, Variant +import pydbus +import time +import sys +import json + +""" +Librem 5, phosh, python. Give me code to read current battery level. + +(and more) + +Lets make it class Phone, one method would be reading battery information, one would be reading operator name / signal strength, one would be getting wifi enabled/disabled / AP name. + +Can you also get silent mode, pending notifications, and gps coordinates on request? + +run this with sudo to work around permission problems + +sudo apt install python3-pydbus + +sudo mmcli --list-modems +sudo mmcli -m 6 --location-enable-gps-nmea --location-enable-gps-raw +""" + + + +class Phone: + verbose = False + + def __init__(self): + self.bus = pydbus.SystemBus() + + def init_sess(self): + self.sess = pydbus.SessionBus() + + # --- Battery --- + def get_battery_info(self): + upower = self.bus.get("org.freedesktop.UPower") + for dev_path in upower.EnumerateDevices(): + if self.verbose: print("dev_path is", dev_path) + dev = self.bus.get(".UPower", dev_path) + if dev.Type == 2: # battery + return { + "percentage": dev.Percentage, + "state": dev.State, + "charging": dev.State == 1, + "time_to_empty": dev.TimeToEmpty, # seconds, 0 if unknown + "time_to_full": dev.TimeToFull, # seconds, 0 if unknown + } + return None + + # --- Vibration --- + # https://github.com/agx/feedbackd/blob/main/examples/example.py + def set_vibration(self, enable: bool): + # Connect to GSettings backend (org.gnome.SettingsDaemon, commonly) + dconf = self.sess.get("org.sigxcpu.Feedback", "/org/sigxcpu/Feedback") + + # Use the standard Properties interface + iface = dconf["org.sigxcpu.Feedback.Haptic"] + + # Example pattern: list of (duration, strength) + pattern = [ + (3.0, 1), + (1.0, 200), + (0.0, 50), + (0.5, 300), + ] + + iface.Vibrate("org.foo.app", pattern) + print(dir(iface)) + + # --- Feedback: silent/full/... --- + # broken + def set_feedback_theme(self, value): + # Connect to GSettings backend (org.gnome.SettingsDaemon, commonly) + dconf = self.bus.get("org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/Dbus") + + # Use the standard Properties interface + iface = dconf["org.freedesktop.DBus.Properties"] + + # Set the key (schema, key, value) + # GVariant format: value must match the expected type, here 's' = string + value = Variant("s", "custom") + + iface.Set("org.sigxcpu.feedbackd", "theme", value) + + # --- Mobile network --- + # Works as root + def get_mobile_info(self): + loc = None + mm = self.bus.get("org.freedesktop.ModemManager1") + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get(".ModemManager1", modem_path) + print("modem ", modem) + operator = getattr(modem, "OperatorName", None) + print("Operator code:", getattr(modem, "OperatorCode", None)) # 0..11 according to MMState + print("State:", getattr(modem, "State", None)) # 0..11 according to MMState + print("Access Technology:", getattr(modem, "AccessTechnologies", None)) + print("Model:", getattr(modem, "Model", None)) + print("Manufacturer:", getattr(modem, "Manufacturer", None)) + print("Revision:", getattr(modem, "Revision", None)) + print("Equipment Identifier (IMEI):", getattr(modem, "EquipmentIdentifier", None)) + + print("Signal (gsm):", getattr(modem, "Gsm", None)) + print("Signal (umts):", getattr(modem, "Umts", None)) + print("Signal (lte):", getattr(modem, "Lte", None)) + + print("Signal:", getattr(modem, "SignalQuality", None)) + print("RegistrationState:", getattr(modem, "RegistrationState", None)) + + # Hallucination? + lac = getattr(modem, "LocationAreaCode", None) + cid = getattr(modem, "CellId", None) + tac = getattr(modem, "TrackingAreaCode", None) + print("Lac...:", lac, cid, tac) + + loc = getattr(modem, "Location", None) + print("Location:", loc) + + v = modem.Setup(0x027, False) + print("Location setup? ", v) + v = modem.GetLocation() + # This has 1) network info and 4) nmea + print(v) + + # Fails with no signal; but has even timing-advance info (I guess only when transmitting) + # It also seems to have neighbouring cells! + """ + Field Meaning Example + operator-id MCC+MNC, identifies mobile operator 23003 + serving Whether device is currently connected to this cell True + physical-ci LTE Physical Cell ID (PCI) 12 + ci LTE Cell Identity XXXXXX + tac Tracking Area Code XXXX + earfcn LTE frequency channel XXXX + cell-type Cell type code (macro/micro/etc.) 5 + rsrp Signal strength (dBm) -122.7 + rsrq Signal quality (dB) -17.0 + """ + try: + v = modem.GetCellInfo() + except: + v = {} + print(v) + + if False: + simple = self.bus.get(".ModemManager1.Modem.Modem3gpp", modem_path) + print(simple) + + if False: + # --- Signal --- + try: + modem3gpp = modem.Modem3gpp + if modem3gpp: + print("3GPP Operator Code:", getattr(modem3gpp, "OperatorCode", None)) + print("Signal Quality:", getattr(modem3gpp, "SignalQuality", None)) # (percent, valid) + print("Registration State:", getattr(modem3gpp, "RegistrationState", None)) + except Exception: + print("No 3gpp?") + + # Pokud je LTE/other, ModemManager má ještě Modem4g nebo ModemSignal + try: + signal = modem.Signal + if signal: + # SignalQuality může být tuple (percent, valid) + print("SignalQuality (Signal interface):", getattr(signal, "SignalQuality", None)) + except Exception: + print("No signal?") + + signal = None + if False: + try: + signal = modem.Signal.Get()["rssi"] + except Exception as e: + return {"error": str(e)} + return {"operator": operator, "signal_strength": signal} + return loc + + def get_mobile_loc(self): + loc = None + mm = self.bus.get("org.freedesktop.ModemManager1") + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get(".ModemManager1", modem_path) + loc = modem.GetLocation() + return loc + + def get_cell_signal(self): + loc = None + mm = self.bus.get("org.freedesktop.ModemManager1") + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get(".ModemManager1", modem_path) + + loc = {} + + def attr(v): + loc[v] = getattr(modem, v, None) + + attr("OperatorName") + attr("OperatorCode") # 0..11 according to MMState + attr("State") # 0..11 according to MMState + attr("AccessTechnologies") + attr("Model") + attr("Manufacturer") + attr("Revision") + attr("EquipmentIdentifier") + + attr("Gsm") + attr("Umts") + attr("Lte") + + attr("SignalQuality") + attr("RegistrationState") + + return loc + + def start_call(self, num): + mm = self.bus.get("org.freedesktop.ModemManager1") + + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get("org.freedesktop.ModemManager1", modem_path) + voice = modem["org.freedesktop.ModemManager1.Modem.Voice"] + + call_properties = { + "number": Variant('s', num) + } + + call_path = voice.CreateCall(call_properties) + #call = self.bus.get("org.freedesktop.ModemManager1", call_path) + #call_iface = call["org.freedesktop.ModemManager1.Call"] + #call_iface.Start() + + return { "call": call_path } + + def send_sms(self, num, text): + mm = self.bus.get("org.freedesktop.ModemManager1") + + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get("org.freedesktop.ModemManager1", modem_path) + messaging = modem["org.freedesktop.ModemManager1.Modem.Messaging"] + + sms_properties = { + "number": Variant('s', num), + "text": Variant('s', text) + } + + sms_path = messaging.Create(sms_properties) + sms = self.bus.get("org.freedesktop.ModemManager1", sms_path) + sms_iface = sms["org.freedesktop.ModemManager1.Sms"] + sms_iface.Send() + + return { "sms": sms_path } + + # 0x01 = 3GPP LAC/CI + # 0x02 = GPS NMEA + # 0x04 = GPS RAW + # 0x08 = CDMA BS + # 0x10 = GPS Unmanaged + CELL_ID = 0x01 + GPS_NMEA = 0x02 + GPS_RAW = 0x04 + + def enable_mobile_loc(self, gps_on, cell_on): + """ + Enable GPS RAW + NMEA. + """ + mm = self.bus.get("org.freedesktop.ModemManager1") + for modem_path in mm.GetManagedObjects(): + modem = self.bus.get(".ModemManager1", modem_path) + + # Setup(uint32 sources, boolean signal_location) + # signal_location=True makes ModemManager emit LocationUpdated signals + if gps_on: + sources = self.GPS_NMEA | self.GPS_RAW + else: + sources = 0 + if cell_on: + sources |= self.CELL_ID; + modem.Setup(sources, True) + + continue + # Optional: explicitly enable (some modems require it) + try: + modem.SetEnable(True) + except Exception: + print("Cant setenable") + return { 'result' : 'setenable failed' } + return { 'result': 'ok' } + + # --- WiFi --- + def get_wifi_info(self): + nm = self.bus.get("org.freedesktop.NetworkManager") + wifi_enabled = nm.WirelessEnabled + active_ssid = None + for conn_path in nm.ActiveConnections: + ac = self.bus.get(".NetworkManager", conn_path) + if ac.Type == "802-11-wireless": + # Step 1: get the settings connection path + settings_path = ac.Connection + # Step 2: fetch the settings object + sc = self.bus.get(".NetworkManager", settings_path) + settings = sc.GetSettings() + ssid = settings["802-11-wireless"]["ssid"] + if isinstance(ssid, (bytes, bytearray)): + ssid = ssid.decode("utf-8", errors="ignore") + else: + ssid = ''.join(chr(c) for c in ssid) + return {"enabled": nm.WirelessEnabled, "ssid": ssid} + + return {"enabled": wifi_enabled, "ssid": active_ssid} + + # --- Silent mode / Do Not Disturb --- + # broken + def get_silent_mode(self): + try: + portal = self.bus.get("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop") + return portal.Settings.Read("org.freedesktop.appearance", + "sound-theme-enabled") == 0 + except Exception as e: + return {"error": str(e)} + + # --- Pending notifications --- + # broken + def get_notifications(self): + try: + notif = self.bus.get("org.freedesktop.Notifications") + # org.freedesktop.Notifications has no standard "list" API, + # Phosh implements its own. + # In phosh, you can query /org/gnome/Notifications for backlog. + phosh_notif = self.bus.get("org.gnome.Shell", + "/org/gnome/Shell/Notifications") + return phosh_notif.ListNotifications() + except Exception as e: + return {"error": str(e)} + + # --- GPS coordinates --- + # Needs permissions from .desktop + def get_location(self): + try: + geoclue = self.bus.get("org.freedesktop.GeoClue2", + "/org/freedesktop/GeoClue2/Manager") + # Step 1: get a client object path + client_path = geoclue.GetClient() + client = self.bus.get("org.freedesktop.GeoClue2", client_path) + + # Step 2: set required properties + client.DesktopId = "phone.py" + client.RequestedAccuracyLevel = 3 # 3 = city-level accuracy + client.Start() # start location updates + + # Step 3: read location + loc_path = client.Location + location = self.bus.get("org.freedesktop.GeoClue2", loc_path) + + return { + "latitude": location.Latitude, + "longitude": location.Longitude, + "accuracy": location.Accuracy, + } + except Exception as e: + return {"error": str(e)} + + # --- Hardware sensors (accelerometer, gyroscope, light, proximity) --- + def get_hardware_sensors(self): + try: + obj = self.bus.get("net.hadess.SensorProxy", "/net/hadess/SensorProxy") + + # obj exposes multiple interfaces; access the one we need + sensor_proxy = obj["net.hadess.SensorProxy"] + + # Enable accelerometer + sensor_proxy.ClaimAccelerometer() + sensor_proxy.ClaimLight() + sensor_proxy.ClaimProximity() + + # Give it a small delay to start updating + time.sleep(0.5) + + sensors = {} + #print(dir(sensor_proxy)) + print('tilt -- tells you phone position -- ', sensor_proxy.AccelerometerTilt) + print('orient -- orientation for screen rotation -- ', sensor_proxy.AccelerometerOrientation) + + # Ambient light + if sensor_proxy.HasAmbientLight: + sensors['ambient_light'] = { + 'lux': sensor_proxy.LightLevel + } + + # Proximity + if sensor_proxy.HasProximity: + sensors['proximity'] = { + 'near': sensor_proxy.ProximityNear + } + + return sensors + except Exception as e: + return {"error": str(e)} + + # --- Screen lock --- + def get_screen_lock(self): + # This one complains + #screensaver = self.sess.get("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver") + screensaver = self.sess.get("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver") + print(dir(screensaver)) + #screensaver.SetActive(True) + return { "Locked": screensaver.GetActive() } + + +# bus = SystemBus() +# login1 = bus.get("org.freedesktop.login1", "/org/freedesktop/login1") +# login1.Suspend(False) # False = interactive, True = force +# login1.Hibernate(False) + +phone = Phone() + +def handle_cmd(v, a): + if v == "bat": + print(json.dumps(phone.get_battery_info())) + sys.exit(0) + if v == "loc": + print(json.dumps(phone.get_mobile_loc())) + sys.exit(0) + if v == "loc_on": + print(json.dumps(phone.enable_mobile_loc(True, True))) + sys.exit(0) + if v == "loc_off": + print(json.dumps(phone.enable_mobile_loc(False, False))) + sys.exit(0) + if v == "signal": + print(json.dumps(phone.get_cell_signal())) + sys.exit(0) + if v == "call": + print(json.dumps(phone.start_call(a[2]))) + sys.exit(0) + if v == "sms": + print(json.dumps(phone.send_sms(a[2], a[3]))) + sys.exit(0) + print("Unknown command "+v) + sys.exit(1) + +if len(sys.argv) > 1: + handle_cmd(sys.argv[1], sys.argv) + +def full(): + phone.init_sess() + print("Battery:", phone.get_battery_info()) + phone.set_vibration(True) +# print("Mobile:", phone.get_mobile_info()) + print("WiFi:", phone.get_wifi_info()) +# print("Silent mode:", phone.get_silent_mode()) +# print("Notifications:", phone.get_notifications()) + print("Location:", phone.get_location()) + print("Hardware sensors:", phone.get_hardware_sensors()) + print("Screen lock:", phone.get_screen_lock()) + phone.set_vibration(False) + # full, quiet, silent +# phone.set_feedback_theme("full") + +def as_root(): + print("Battery:", phone.get_battery_info()) + print("Mobile:", phone.get_mobile_info()) + print("WiFi:", phone.get_wifi_info()) +# print("Silent mode:", phone.get_silent_mode()) +# print("Notifications:", phone.get_notifications()) +# print("Location:", phone.get_location()) + print("Hardware sensors:", phone.get_hardware_sensors()) + # full, quiet, silent +# phone.set_feedback_theme("full") + +#full() +as_root() From 85823fc58c024c843638ab55bda0e09150ff7d1a Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 9 Mar 2026 10:10:24 +0100 Subject: [PATCH 09/11] cellular: Add metadata --- .../META-INF/MANIFEST.JSON | 12 ++++++------ .../res/mipmap-mdpi/icon_64x64.png | Bin 6765 -> 8683 bytes 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON index 9e6b0742..5456fb6d 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/META-INF/MANIFEST.JSON @@ -1,11 +1,11 @@ { -"name": "Xxx", +"name": "Cellular", "publisher": "Pavel Machek", -"short_description": "Xxx", -"long_description": "Simple xxx app.", -"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/icons/cz.ucw.pavel.xxx_0.0.1_64x64.png", -"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.xxx/mpks/cz.ucw.pavel.xxx_0.0.1.mpk", -"fullname": "cz.ucw.pavel.xxx", +"short_description": "Application for placing phone calls", +"long_description": "Simple application for monitoring network state and placing phone calls.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.cellular/icons/cz.ucw.pavel.cellular_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.cellular/mpks/cz.ucw.pavel.cellular_0.0.1.mpk", +"fullname": "cz.ucw.pavel.cellular", "version": "0.0.1", "category": "utilities", "activities": [ diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.cellular/res/mipmap-mdpi/icon_64x64.png index 316c5f2366bdcab259ac9fb75ce7c3db1dfc4c65..662b3c89ded8f291bb9494fba13dedfe2ce17ff4 100644 GIT binary patch delta 4356 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 From d6ffabdb4baba9fd64c0d2f7f31e304d6225fa6b Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 9 Mar 2026 11:38:44 +0100 Subject: [PATCH 10/11] cellular: remove unused glue --- .../cz.ucw.pavel.cellular/assets/phone.py | 305 ------------------ 1 file changed, 305 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py index 41dc7813..948c19ff 100755 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/phone.py @@ -6,16 +6,8 @@ import sys import json """ -Librem 5, phosh, python. Give me code to read current battery level. - -(and more) - Lets make it class Phone, one method would be reading battery information, one would be reading operator name / signal strength, one would be getting wifi enabled/disabled / AP name. -Can you also get silent mode, pending notifications, and gps coordinates on request? - -run this with sudo to work around permission problems - sudo apt install python3-pydbus sudo mmcli --list-modems @@ -33,149 +25,6 @@ class Phone: def init_sess(self): self.sess = pydbus.SessionBus() - # --- Battery --- - def get_battery_info(self): - upower = self.bus.get("org.freedesktop.UPower") - for dev_path in upower.EnumerateDevices(): - if self.verbose: print("dev_path is", dev_path) - dev = self.bus.get(".UPower", dev_path) - if dev.Type == 2: # battery - return { - "percentage": dev.Percentage, - "state": dev.State, - "charging": dev.State == 1, - "time_to_empty": dev.TimeToEmpty, # seconds, 0 if unknown - "time_to_full": dev.TimeToFull, # seconds, 0 if unknown - } - return None - - # --- Vibration --- - # https://github.com/agx/feedbackd/blob/main/examples/example.py - def set_vibration(self, enable: bool): - # Connect to GSettings backend (org.gnome.SettingsDaemon, commonly) - dconf = self.sess.get("org.sigxcpu.Feedback", "/org/sigxcpu/Feedback") - - # Use the standard Properties interface - iface = dconf["org.sigxcpu.Feedback.Haptic"] - - # Example pattern: list of (duration, strength) - pattern = [ - (3.0, 1), - (1.0, 200), - (0.0, 50), - (0.5, 300), - ] - - iface.Vibrate("org.foo.app", pattern) - print(dir(iface)) - - # --- Feedback: silent/full/... --- - # broken - def set_feedback_theme(self, value): - # Connect to GSettings backend (org.gnome.SettingsDaemon, commonly) - dconf = self.bus.get("org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/Dbus") - - # Use the standard Properties interface - iface = dconf["org.freedesktop.DBus.Properties"] - - # Set the key (schema, key, value) - # GVariant format: value must match the expected type, here 's' = string - value = Variant("s", "custom") - - iface.Set("org.sigxcpu.feedbackd", "theme", value) - - # --- Mobile network --- - # Works as root - def get_mobile_info(self): - loc = None - mm = self.bus.get("org.freedesktop.ModemManager1") - for modem_path in mm.GetManagedObjects(): - modem = self.bus.get(".ModemManager1", modem_path) - print("modem ", modem) - operator = getattr(modem, "OperatorName", None) - print("Operator code:", getattr(modem, "OperatorCode", None)) # 0..11 according to MMState - print("State:", getattr(modem, "State", None)) # 0..11 according to MMState - print("Access Technology:", getattr(modem, "AccessTechnologies", None)) - print("Model:", getattr(modem, "Model", None)) - print("Manufacturer:", getattr(modem, "Manufacturer", None)) - print("Revision:", getattr(modem, "Revision", None)) - print("Equipment Identifier (IMEI):", getattr(modem, "EquipmentIdentifier", None)) - - print("Signal (gsm):", getattr(modem, "Gsm", None)) - print("Signal (umts):", getattr(modem, "Umts", None)) - print("Signal (lte):", getattr(modem, "Lte", None)) - - print("Signal:", getattr(modem, "SignalQuality", None)) - print("RegistrationState:", getattr(modem, "RegistrationState", None)) - - # Hallucination? - lac = getattr(modem, "LocationAreaCode", None) - cid = getattr(modem, "CellId", None) - tac = getattr(modem, "TrackingAreaCode", None) - print("Lac...:", lac, cid, tac) - - loc = getattr(modem, "Location", None) - print("Location:", loc) - - v = modem.Setup(0x027, False) - print("Location setup? ", v) - v = modem.GetLocation() - # This has 1) network info and 4) nmea - print(v) - - # Fails with no signal; but has even timing-advance info (I guess only when transmitting) - # It also seems to have neighbouring cells! - """ - Field Meaning Example - operator-id MCC+MNC, identifies mobile operator 23003 - serving Whether device is currently connected to this cell True - physical-ci LTE Physical Cell ID (PCI) 12 - ci LTE Cell Identity XXXXXX - tac Tracking Area Code XXXX - earfcn LTE frequency channel XXXX - cell-type Cell type code (macro/micro/etc.) 5 - rsrp Signal strength (dBm) -122.7 - rsrq Signal quality (dB) -17.0 - """ - try: - v = modem.GetCellInfo() - except: - v = {} - print(v) - - if False: - simple = self.bus.get(".ModemManager1.Modem.Modem3gpp", modem_path) - print(simple) - - if False: - # --- Signal --- - try: - modem3gpp = modem.Modem3gpp - if modem3gpp: - print("3GPP Operator Code:", getattr(modem3gpp, "OperatorCode", None)) - print("Signal Quality:", getattr(modem3gpp, "SignalQuality", None)) # (percent, valid) - print("Registration State:", getattr(modem3gpp, "RegistrationState", None)) - except Exception: - print("No 3gpp?") - - # Pokud je LTE/other, ModemManager má ještě Modem4g nebo ModemSignal - try: - signal = modem.Signal - if signal: - # SignalQuality může být tuple (percent, valid) - print("SignalQuality (Signal interface):", getattr(signal, "SignalQuality", None)) - except Exception: - print("No signal?") - - signal = None - if False: - try: - signal = modem.Signal.Get()["rssi"] - except Exception as e: - return {"error": str(e)} - return {"operator": operator, "signal_strength": signal} - return loc - def get_mobile_loc(self): loc = None mm = self.bus.get("org.freedesktop.ModemManager1") @@ -286,132 +135,6 @@ class Phone: return { 'result' : 'setenable failed' } return { 'result': 'ok' } - # --- WiFi --- - def get_wifi_info(self): - nm = self.bus.get("org.freedesktop.NetworkManager") - wifi_enabled = nm.WirelessEnabled - active_ssid = None - for conn_path in nm.ActiveConnections: - ac = self.bus.get(".NetworkManager", conn_path) - if ac.Type == "802-11-wireless": - # Step 1: get the settings connection path - settings_path = ac.Connection - # Step 2: fetch the settings object - sc = self.bus.get(".NetworkManager", settings_path) - settings = sc.GetSettings() - ssid = settings["802-11-wireless"]["ssid"] - if isinstance(ssid, (bytes, bytearray)): - ssid = ssid.decode("utf-8", errors="ignore") - else: - ssid = ''.join(chr(c) for c in ssid) - return {"enabled": nm.WirelessEnabled, "ssid": ssid} - - return {"enabled": wifi_enabled, "ssid": active_ssid} - - # --- Silent mode / Do Not Disturb --- - # broken - def get_silent_mode(self): - try: - portal = self.bus.get("org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop") - return portal.Settings.Read("org.freedesktop.appearance", - "sound-theme-enabled") == 0 - except Exception as e: - return {"error": str(e)} - - # --- Pending notifications --- - # broken - def get_notifications(self): - try: - notif = self.bus.get("org.freedesktop.Notifications") - # org.freedesktop.Notifications has no standard "list" API, - # Phosh implements its own. - # In phosh, you can query /org/gnome/Notifications for backlog. - phosh_notif = self.bus.get("org.gnome.Shell", - "/org/gnome/Shell/Notifications") - return phosh_notif.ListNotifications() - except Exception as e: - return {"error": str(e)} - - # --- GPS coordinates --- - # Needs permissions from .desktop - def get_location(self): - try: - geoclue = self.bus.get("org.freedesktop.GeoClue2", - "/org/freedesktop/GeoClue2/Manager") - # Step 1: get a client object path - client_path = geoclue.GetClient() - client = self.bus.get("org.freedesktop.GeoClue2", client_path) - - # Step 2: set required properties - client.DesktopId = "phone.py" - client.RequestedAccuracyLevel = 3 # 3 = city-level accuracy - client.Start() # start location updates - - # Step 3: read location - loc_path = client.Location - location = self.bus.get("org.freedesktop.GeoClue2", loc_path) - - return { - "latitude": location.Latitude, - "longitude": location.Longitude, - "accuracy": location.Accuracy, - } - except Exception as e: - return {"error": str(e)} - - # --- Hardware sensors (accelerometer, gyroscope, light, proximity) --- - def get_hardware_sensors(self): - try: - obj = self.bus.get("net.hadess.SensorProxy", "/net/hadess/SensorProxy") - - # obj exposes multiple interfaces; access the one we need - sensor_proxy = obj["net.hadess.SensorProxy"] - - # Enable accelerometer - sensor_proxy.ClaimAccelerometer() - sensor_proxy.ClaimLight() - sensor_proxy.ClaimProximity() - - # Give it a small delay to start updating - time.sleep(0.5) - - sensors = {} - #print(dir(sensor_proxy)) - print('tilt -- tells you phone position -- ', sensor_proxy.AccelerometerTilt) - print('orient -- orientation for screen rotation -- ', sensor_proxy.AccelerometerOrientation) - - # Ambient light - if sensor_proxy.HasAmbientLight: - sensors['ambient_light'] = { - 'lux': sensor_proxy.LightLevel - } - - # Proximity - if sensor_proxy.HasProximity: - sensors['proximity'] = { - 'near': sensor_proxy.ProximityNear - } - - return sensors - except Exception as e: - return {"error": str(e)} - - # --- Screen lock --- - def get_screen_lock(self): - # This one complains - #screensaver = self.sess.get("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver") - screensaver = self.sess.get("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver") - print(dir(screensaver)) - #screensaver.SetActive(True) - return { "Locked": screensaver.GetActive() } - - -# bus = SystemBus() -# login1 = bus.get("org.freedesktop.login1", "/org/freedesktop/login1") -# login1.Suspend(False) # False = interactive, True = force -# login1.Hibernate(False) - phone = Phone() def handle_cmd(v, a): @@ -442,31 +165,3 @@ def handle_cmd(v, a): if len(sys.argv) > 1: handle_cmd(sys.argv[1], sys.argv) -def full(): - phone.init_sess() - print("Battery:", phone.get_battery_info()) - phone.set_vibration(True) -# print("Mobile:", phone.get_mobile_info()) - print("WiFi:", phone.get_wifi_info()) -# print("Silent mode:", phone.get_silent_mode()) -# print("Notifications:", phone.get_notifications()) - print("Location:", phone.get_location()) - print("Hardware sensors:", phone.get_hardware_sensors()) - print("Screen lock:", phone.get_screen_lock()) - phone.set_vibration(False) - # full, quiet, silent -# phone.set_feedback_theme("full") - -def as_root(): - print("Battery:", phone.get_battery_info()) - print("Mobile:", phone.get_mobile_info()) - print("WiFi:", phone.get_wifi_info()) -# print("Silent mode:", phone.get_silent_mode()) -# print("Notifications:", phone.get_notifications()) -# print("Location:", phone.get_location()) - print("Hardware sensors:", phone.get_hardware_sensors()) - # full, quiet, silent -# phone.set_feedback_theme("full") - -#full() -as_root() From 439dc083209462b92b363ffbe8fc27433fe1562a Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Mon, 9 Mar 2026 11:39:02 +0100 Subject: [PATCH 11/11] cellular: add some comments --- .../apps/cz.ucw.pavel.cellular/assets/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py index ddd1ebfd..a6877e50 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py +++ b/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/main.py @@ -1,7 +1,7 @@ from mpos import Activity """ - +Simple cellular-network example """ import time @@ -49,7 +49,7 @@ class CellularManager: cm = CellularManager() # ------------------------------------------------------------ -# +# User interface # ------------------------------------------------------------ class Main(Activity): @@ -85,7 +85,7 @@ class Main(Activity): self.number.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 12) self.call = lv.button(self.screen) - self.call.align_to(self.number, lv.ALIGN.OUT_RIGHT_MID, 10, 0) + self.call.align_to(self.number, lv.ALIGN.OUT_RIGHT_MID, 2, 0) self.call.add_event_cb(lambda e: self.on_call(), lv.EVENT.CLICKED, None) # Two text areas on single screen don't work well.