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 @@ def onCreate(self): # 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 @@ def onCreate(self): 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 @@ def tick(self, t): 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 @@ def onCreate(self): 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 @@ def tick(self, t): 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 @@ def tick(self, t): 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 @@ def onCreate(self): 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 @@ def poll(self): 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 @@ def onCreate(self): 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 @@ def onCreate(self): 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 @@ def onPause(self, screen): # -------------------- + 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 @@ def onCreate(self): 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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 delta 2638 zcmV-U3bFO;L+vz>BYy|adQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+O=3$mV`PE z{nsjH2{_x!aXjbkV3t2uGEh)O#eUOtVQ`QoT_r>PzyA#S5B^NFmaGfWC+`>h*<_Q> zc!pD=*18Lns$2&sn zMus`kcxWr3v=TCU4yctto?RPUQaG257H2bQ-O@QD(ti z8$B5?{PSe8JY)=eOjO3?d@bmFf5PvR}Dt z<&&uRu(PTA&VQ%=s&D!|Dw%&{v`Az0DP6W6D)p;Sn<(;xi3KV*vdrTO_&GcWxe~5O zu-*!E+ESN-tE!|lU>VA)rm`BUOIsCK(pGmJ&}v<^k-lhxPP&?ffRp7*4IO}0GZC0o zz-!Q}cV3k2n`FDHA~tRcGqnOS#il@f`!}wqgvMZ#SAS}WyRN{OW+Ma}j2z!F3xKGx z*^v_kZ`b`F<7$AQ6E#N&n3pn16sK0(VmmSdw82D&cDjWMZ6yE|VQ&X!lmP;Zhw&?~ zmY{V|3VMvdCxcMb#_9qHlyka)OKlCjCFrPmZ>$MQ=4z*BH)0lmaK@VeXy8o1iUS+$ zC)fxZ%74x{>zwm0xagA0-gxVs_da+IemR(6g9|={5JL(%A~Z%9eGD;DOfe^&41%Le zK7|xhN;zZG49Xd+GbZF*bTP#iS9}R2mQ-?u`BYn7^)=K;HPzg7GYxT~`4(DispYOZ zOsU)My6>UKo_ZcIwPA)GZuk*K9BJeoHANHFhkxsH)YOKW7NmG$*ipmM5H=Eu!--?e zfS9%lhzCXh2CW&hB`Rw*F?5XCk~F-+X>jQnoTeBtAZWYNZm1o*8*(Sy$l5>QrZ?n> zq5C(GBZlq?a*w!Opw>9s)Wwh>#BRt+T@2h06MD?LbRfft(v78-(Uq(G4)%ao8>3j$ z+kfZ|ucd@Ut{Cr!Olid=47G-&XsYyel~Ncz`_6AQZ!@zQ#~}<54-6Uc37n?nuzTxs zL2My%`VB?itKsm1Ya?m4EMVMkiHGj~}a3(+k+_|rK#0MrVL(6g-1R;hA+1X7B zoHMQ_j+MqGaS))L!gC&2PXSWTnY`!48@(F*;LY`w1$xHn2b`X?MQ1JCC~R9pPyFK= z$H|a#FzY7V@WVDhGGb)kdTj~=K?J#2y_{t!jAv$cq9EpI8(6Se2qr`v%yODYh|3FivrZ!FsYDj@IMC zUTc?e^H~J3v3o3(4Lh%D_t`>agueP4Vqi^UOJF%YQs1o^S!q zx3)x0Sp9=7fkk8&BdCma|1b+eJ2!DkHEj~erQws`QE55^l?g4<75PVlHZTi2{{?yW0|Fi*M z_Vqs@I4Di#7`C+q7X7<;&Hhc_#_MNcn7)oiPh5$`dbT(q z>X9w!5dR=T69@bSJ=)-VL~pjHO*kf3sN(}iA2!wExOS09*AYfQ9e)RN(N2*`J_EH2 ziU<2hGnSvKVA{&y_9BeVD9Q!bGwP<%XNq^*j3kulv&-^jMxk%q z;FbH*XJyoT_od&;s((km>GP^+d8xnuR~UI)hRWAcC&kIRB`)pznT697D zvfWQHXRdZ8=ouH^s-9kl^v|K7zf?y@zUX8VMZQ7&WKZ;Wp{7jqt59ps+CvRV(mKZ) zF~nQ6;dpcmHAv}FvrCoNwkS$hVf0-ZxsS^Hh#K<6Tuy4N0e|NDV9@9}bky3A0Yk#;-u&zC4Mg{w21ZK zxDW5{J?`ECLbb>=t8E<6blXhCqGBq&Dh6I5pbtG5LZ{3uV@{IdXgR*_;p6LFglDxr z_vh$VGZq7UB7gBLGfbO!gLrz=HaPDShge=#iO-40Ou8WPBi9v=-#F)77IteDHam6A8p|ubo~;!6mk{7$gzMDG{~+W{11M2Yky@YC%mL!6li~OoR1M8unW{G zj`Mx&IJFbN{|sE|4S%T$%zTnwX=vdipm!U%xNd059&ot>3_KaKDZ7%NrjX47?`QN) zX`ufW=vwo7tMB9V0Z39;@f+aa5E#u-_IitVcQp6*@0ogkKOH-iB9Rocnh8F+;04GUAK~#9!?ZE*J0002O wp!ZL04mY3x000000000000000004k@13*9k0k3)v=l}o!07*qoM6N<$f_+8HLI3~& 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 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 @@ def __init__(self): 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 @@ def enable_mobile_loc(self, gps_on, cell_on): 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 @@ def sms(self, num, text): cm = CellularManager() # ------------------------------------------------------------ -# +# User interface # ------------------------------------------------------------ class Main(Activity): @@ -85,7 +85,7 @@ def onCreate(self): 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.