This commit is contained in:
Thomas Farstrike
2026-03-11 14:09:13 +01:00
24 changed files with 4401 additions and 88 deletions
@@ -0,0 +1,24 @@
{
"name": "Cellular",
"publisher": "Pavel Machek",
"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": [
{
"entrypoint": "assets/main.py",
"classname": "Main",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
@@ -0,0 +1,151 @@
from mpos import Activity
"""
Simple cellular-network example
"""
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/internal_filesystem/apps/cz.ucw.pavel.cellular/assets/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
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()
# ------------------------------------------------------------
# User interface
# ------------------------------------------------------------
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_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.set_style_text_font(lv.font_montserrat_20, 0)
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.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, 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.
# 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))
self.setContentView(self.screen)
cm.init()
def onResume(self, screen):
self.timer = lv.timer_create(self.tick, 60000, None)
self.tick(0)
def onPause(self, screen):
if self.timer:
self.timer.delete()
self.timer = None
# --------------------
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]
hh, mm, ss = now[3], now[4], now[5]
self.lbl_month.set_text("busy")
cm.poll()
s = ""
s += cm.signal["OperatorName"] + "\n"
s += "RegistrationState %d\n" % cm.signal["RegistrationState"]
s += "State %d " % 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))
# --------------------
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
from pydbus import SystemBus, Variant
import pydbus
import time
import sys
import json
"""
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.
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()
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' }
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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

@@ -0,0 +1,24 @@
{
"name": "Floodit",
"publisher": "Pavel Machek",
"short_description": "Simple game with colors.",
"long_description": "Game with colors, where objective is to turn whole board into single color in minimum number of steps.",
"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.floodit/icons/cz.ucw.pavel.floodit_0.0.1_64x64.png",
"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.floodit/mpks/cz.ucw.pavel.floodit_0.0.1.mpk",
"fullname": "cz.ucw.pavel.floodit",
"version": "0.0.1",
"category": "utilities",
"activities": [
{
"entrypoint": "assets/main.py",
"classname": "Main",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
@@ -0,0 +1,210 @@
import time
import random
"""
Flood-It game
Fill the entire board with a single color
using the smallest number of moves.
Touch a color button to flood the region
starting from the top-left corner.
"""
from mpos import Activity
try:
import lvgl as lv
except ImportError:
pass
class Main(Activity):
COLS = 10
ROWS = 10
COLORS = [
0xE74C3C, # red
0xF1C40F, # yellow
0x2ECC71, # green
0x3498DB, # blue
0x9B59B6, # purple
0xE67E22, # orange
]
def __init__(self):
super().__init__()
self.board = []
self.cells = []
self.moves = 0
# ---------------------------------------------------------------------
def onCreate(self):
self.screen = lv.obj()
self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE)
font = lv.font_montserrat_20
score = lv.label(self.screen)
score.align(lv.ALIGN.TOP_LEFT, 5, 25)
score.set_text("Moves")
score.set_style_text_font(font, 0)
self.lb_score = score
d = lv.display_get_default()
self.SCREEN_WIDTH = d.get_horizontal_resolution()
self.SCREEN_HEIGHT = d.get_vertical_resolution()
# color buttons
btn_size = 45
spacing = 5
self.CELL = min(
self.SCREEN_WIDTH // (self.COLS + 2),
(self.SCREEN_HEIGHT - btn_size) // (self.ROWS + 3)
)
board_x = (self.SCREEN_WIDTH - self.CELL * self.COLS) // 2
board_y = (self.SCREEN_HEIGHT - self.CELL * self.ROWS) // 2
for r in range(self.ROWS):
row = []
for c in range(self.COLS):
o = lv.obj(self.screen)
o.set_size(self.CELL - 2, self.CELL - 2)
o.set_pos(
board_x + c * self.CELL + 1,
board_y + r * self.CELL + 1 - btn_size // 2
)
o.set_style_radius(4, 0)
o.set_style_border_width(1, 0)
row.append(o)
self.cells.append(row)
for i, col in enumerate(self.COLORS):
btn = lv.button(self.screen)
btn.set_size(btn_size, btn_size)
btn.align(
lv.ALIGN.BOTTOM_LEFT,
5 + i * (btn_size + spacing),
-5
)
btn.set_style_bg_color(lv.color_hex(col), 0)
btn.add_event_cb(
lambda e, c=i: self.pick_color(c),
lv.EVENT.CLICKED,
None
)
focusgroup = lv.group_get_default()
if focusgroup:
focusgroup.add_obj(self.screen)
self.setContentView(self.screen)
self.new_game()
# ---------------------------------------------------------------------
def new_game(self):
self.moves = 0
self.lb_score.set_text("Moves\n0")
self.board = [
[random.randrange(len(self.COLORS)) for _ in range(self.COLS)]
for _ in range(self.ROWS)
]
self.redraw()
# ---------------------------------------------------------------------
def pick_color(self, color):
start_color = self.board[0][0]
if start_color == color:
return
self.flood_fill(start_color, color)
self.moves += 1
self.lb_score.set_text("Moves\n%d" % self.moves)
self.redraw()
if self.check_win():
self.win()
# ---------------------------------------------------------------------
def flood_fill(self, old, new):
stack = [(0, 0)]
while stack:
r, c = stack.pop()
if not (0 <= r < self.ROWS and 0 <= c < self.COLS):
continue
if self.board[r][c] != old:
continue
self.board[r][c] = new
stack.append((r + 1, c))
stack.append((r - 1, c))
stack.append((r, c + 1))
stack.append((r, c - 1))
# ---------------------------------------------------------------------
def check_win(self):
color = self.board[0][0]
for r in range(self.ROWS):
for c in range(self.COLS):
if self.board[r][c] != color:
return False
return True
# ---------------------------------------------------------------------
def win(self):
label = lv.label(self.screen)
label.set_text("Finished in %d moves!" % self.moves)
label.center()
# ---------------------------------------------------------------------
def redraw(self):
for r in range(self.ROWS):
for c in range(self.COLS):
v = self.board[r][c]
self.cells[r][c].set_style_bg_color(
lv.color_hex(self.COLORS[v]), 0
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,24 @@
{
"name": "Gyro",
"publisher": "Pavel Machek",
"short_description": "Gyro",
"long_description": "Simple gyro app.",
"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.gyro/icons/cz.ucw.pavel.gyro_0.0.1_64x64.png",
"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.gyro/mpks/cz.ucw.pavel.gyro_0.0.1.mpk",
"fullname": "cz.ucw.pavel.gyro",
"version": "0.0.1",
"category": "utilities",
"activities": [
{
"entrypoint": "assets/main.py",
"classname": "Main",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,24 @@
{
"name": "Navstar",
"publisher": "Pavel Machek",
"short_description": "Simple navigation app.",
"long_description": "Simple navigation app using data from NAVSTAR GPS and other GNSS systems.",
"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"
}
]
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
import lvgl as lv
import mpos
from mpos import Activity, MposKeyboard
# -----------------------------
# Canvas (LVGL)
# -----------------------------
class Canvas:
"""
LVGL canvas + layer drawing Canvas.
This matches ports where:
- lv.canvas has init_layer() / finish_layer()
- primitives are drawn via lv.draw_* into lv.layer_t
"""
def __init__(self, scr, canvas):
self.scr = scr
# Screen size
self.W = scr.get_width()
self.H = scr.get_height()
# Bottom button bar
self.margin = 2
self.bar_h = 39
# Canvas drawing area (everything above button bar)
self.draw_w = self.W
self.draw_h = self.H - (self.bar_h + self.margin * 2)
self.canvas = canvas
# Background: white (change if you want dark theme)
self.canvas.set_style_bg_color(lv.color_white(), lv.PART.MAIN)
# Buffer: your working example uses 4 bytes/pixel
# Reality filter: this depends on LV_COLOR_DEPTH; but your example proves it works.
self.buf = bytearray(self.draw_w * self.draw_h * 4)
self.canvas.set_buffer(self.buf, self.draw_w, self.draw_h, lv.COLOR_FORMAT.NATIVE)
# Layer used for draw engine
self.layer = lv.layer_t()
self.canvas.init_layer(self.layer)
# Persistent draw descriptors (avoid allocations)
self._line_dsc = lv.draw_line_dsc_t()
lv.draw_line_dsc_t.init(self._line_dsc)
self._line_dsc.width = 1
self._line_dsc.color = lv.color_black()
self._line_dsc.round_end = 1
self._line_dsc.round_start = 1
self._label_dsc = lv.draw_label_dsc_t()
lv.draw_label_dsc_t.init(self._label_dsc)
self._label_dsc.color = lv.color_black()
self._label_dsc.font = lv.font_montserrat_24
self._rect_dsc = lv.draw_rect_dsc_t()
lv.draw_rect_dsc_t.init(self._rect_dsc)
self._rect_dsc.bg_opa = lv.OPA.TRANSP
self._rect_dsc.border_opa = lv.OPA.COVER
self._rect_dsc.border_width = 1
self._rect_dsc.border_color = lv.color_black()
self._fill_dsc = lv.draw_rect_dsc_t()
lv.draw_rect_dsc_t.init(self._fill_dsc)
self._fill_dsc.bg_opa = lv.OPA.COVER
self._fill_dsc.bg_color = lv.color_black()
self._fill_dsc.border_width = 1
# Clear once
self.clear()
# ----------------------------
# Layer lifecycle
# ----------------------------
def _begin(self):
# Start drawing into the layer
self.canvas.init_layer(self.layer)
def _end(self):
# Commit drawing
self.canvas.finish_layer(self.layer)
# ----------------------------
# Public API: drawing
# ----------------------------
def clear(self):
# Clear the canvas background
self.canvas.fill_bg(lv.color_white(), lv.OPA.COVER)
def text(self, x, y, s, fg = lv.color_black()):
self._begin()
dsc = lv.draw_label_dsc_t()
lv.draw_label_dsc_t.init(dsc)
dsc.text = str(s)
dsc.font = lv.font_montserrat_24
dsc.color = lv.color_black()
area = lv.area_t()
area.x1 = x
area.y1 = y
area.x2 = x + self.W
area.y2 = y + self.H
lv.draw_label(self.layer, dsc, area)
self._end()
def line(self, x1, y1, x2, y2, fg = lv.color_black()):
self._begin()
dsc = self._line_dsc
dsc.p1 = lv.point_precise_t()
dsc.p2 = lv.point_precise_t()
dsc.p1.x = int(x1)
dsc.p1.y = int(y1)
dsc.p2.x = int(x2)
dsc.p2.y = int(y2)
lv.draw_line(self.layer, dsc)
self._end()
def circle(self, x, y, r, fg = lv.color_black()):
# Rounded rectangle trick (works everywhere)
self._begin()
a = lv.area_t()
a.x1 = int(x - r)
a.y1 = int(y - r)
a.x2 = int(x + r)
a.y2 = int(y + r)
dsc = self._rect_dsc
dsc.radius = lv.RADIUS_CIRCLE
dsc.border_color = fg
lv.draw_rect(self.layer, dsc, a)
self._end()
def fill_circle(self, x, y, r, fg = lv.color_black(), bg = lv.color_white()):
self._begin()
a = lv.area_t()
a.x1 = int(x - r)
a.y1 = int(y - r)
a.x2 = int(x + r)
a.y2 = int(y + r)
dsc = self._rect_dsc
dsc.radius = lv.RADIUS_CIRCLE
dsc.border_color = fg
dsc.bg_color = bg
lv.draw_rect(self.layer, dsc, a)
self._end()
def fill_rect(self, x, y, sx, sy, fg = lv.color_black(), bg = lv.color_white()):
self._begin()
a = lv.area_t()
a.x1 = x
a.y1 = y
a.x2 = x+sx
a.y2 = y+sy
dsc = self._fill_dsc
dsc.border_color = fg
dsc.bg_color = bg
lv.draw_rect(self.layer, dsc, a)
self._end()
def update(self):
# Nothing needed; drawing is committed per primitive.
# If you want, you can change the implementation so that:
# - draw ops happen between clear() and update()
# But then you must ensure the app calls update() once per frame.
pass
# ----------------------------
# App logic
# ----------------------------
class PagedCanvas(Activity):
def __init__(self):
super().__init__()
self.page = 0
self.pages = 3
def onCreate(self):
self.scr = lv.obj()
scr = self.scr
# Screen size
self.W = scr.get_width()
self.H = scr.get_height()
# Bottom button bar
self.margin = 2
self.bar_h = 39
# Canvas drawing area (everything above button bar)
self.draw_w = self.W
self.draw_h = self.H - (self.bar_h + self.margin * 2)
# Canvas
self.canvas = lv.canvas(self.scr)
self.canvas.set_size(self.draw_w, self.draw_h)
self.canvas.align(lv.ALIGN.TOP_LEFT, 0, 0)
self.canvas.set_style_border_width(0, 0)
self.c = Canvas(self.scr, self.canvas)
# Build buttons
self.build_buttons()
self.setContentView(self.c.scr)
# ----------------------------
# Button bar
# ----------------------------
def _make_btn(self, parent, x, y, w, h, label):
b = lv.button(parent)
b.set_pos(x, y)
b.set_size(w, h)
l = lv.label(b)
l.set_text(label)
l.center()
return b
def _btn_cb(self, evt, tag):
self.page = tag
def template_buttons(self, names):
margin = self.margin
y = self.H - self.bar_h - margin
num = len(names)
if num == 0:
self.buttons = []
return
w = (self.W - margin * (num + 1)) // num
h = self.bar_h
x0 = margin
self.buttons = []
for i, label in enumerate(names):
x = x0 + (w + margin) * i
btn = self._make_btn(self.scr, x, y, w, h, label)
# capture index correctly
btn.add_event_cb(
lambda evt, idx=i: self._btn_cb(evt, idx),
lv.EVENT.CLICKED,
None
)
self.buttons.append(btn)
def build_buttons(self):
self.template_buttons(["Pg0", "Pg1", "Pg2", "Pg3", "..."])
def onResume(self, screen):
self.timer = lv.timer_create(self.tick, 1000, None)
def onPause(self, screen):
if self.timer:
self.timer.delete()
self.timer = None
def tick(self, t):
self.update()
self.draw()
def update(self):
pass
def draw_page_example(self):
ui = self.c
ui.clear()
st = 28
y = 2*st
ui.text(0, y, "Hello world, page is %d" % self.page)
y += st
def draw(self):
self.draw_page_example()
def handle_buttons(self):
ui = self.c
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -12,7 +12,7 @@ try:
except ImportError:
pass
from mpos import Activity, MposKeyboard
from mpos import Activity, MposKeyboard, DownloadManager
import ujson
import utime
@@ -55,22 +55,87 @@ class WData:
99: "Thunderstorm + hail",
}
def init(self):
pass
def code_to_text(self, code):
return self.WMO_CODES.get(int(code), "Unknown")
class Hourly(WData):
def __init__(self, cw):
self.temp = cw["temperature_2m"]
self.wind = cw["windspeed"]
self.code = self.code_to_text(cw["weather_code"])
def get(self, v, cw, ind):
if ind == None:
return cw[v]
else:
return cw[v][ind]
def full(self):
return f"{self.code}\nTemp {self.temp:.1f} dew {self.dew:.1f} pres {self.pres:1f}\n" \
f"Precip {self.precip}\nWind {self.wind} gust {self.gust}"
def short(self):
r = f"{self.code} {self.temp:.1f}°C"
if self.dew + 3 > self.temp:
r += f" dew {self.dew:.1f}°C"
if self.gust > self.wind + 5:
r += f" {self.gust:.0f} g"
elif self.wind > 10:
r += f" {self.wind:.0f} w"
# FIXME: add precip
return r
def similar(self, prev):
if self.code != prev.code:
return False
if abs(self.temp - prev.temp) > 3:
return False
if abs(self.wind - prev.wind) > 10:
return False
if abs(self.gust - prev.gust) > 10:
return False
return True
def summarize(self):
return f"{self.code}\nTemp {self.temp}\nWind {self.wind}"
return self.ftime() + self.short()
class Hourly(WData):
def init(self, cw, ind):
super().init()
self.time = None
self.temp = self.get("temperature_2m", cw, ind)
self.dew = self.get("dewpoint_2m", cw, ind)
self.pres = self.get("pressure_msl", cw, ind)
self.precip = self.get("precipitation", cw, ind)
self.wind = self.get("wind_speed_10m", cw, ind)
self.gust = self.get("wind_gusts_10m", cw, ind)
self.raw_code = self.get("weather_code", cw, ind)
self.code = self.code_to_text(self.raw_code)
def ftime(self):
if self.time:
return self.time[11:13] + "h "
return ""
class Daily(WData):
def init(self, cw, ind):
super().init()
self.temp = self.get("temperature_2m_max", cw, ind)
self.temp_min = self.get("temperature_2m_min", cw, ind)
self.dew = self.get("dewpoint_2m_max", cw, ind)
self.dew_min = self.get("dewpoint_2m_min", cw, ind)
self.pres = None
self.precip = self.get("precipitation_sum", cw, ind)
self.wind = self.get("wind_speed_10m_max", cw, ind)
self.gust = self.get("wind_gusts_10m_max", cw, ind)
self.raw_code = self.get("weather_code", cw, ind)
self.code = self.code_to_text(self.raw_code)
def ftime(self):
return self.time[8:10] + ". "
class Weather:
name = "Prague"
lat = 50.08
lon = 14.44
# LKPR airport
lat = 50 + 6/60.
lon = 14 + 15/60.
def __init__(self):
self.now = None
@@ -84,68 +149,102 @@ class Weather:
# See https://open-meteo.com/en/docs?forecast_days=1&current=relative_humidity_2m
host = "api.open-meteo.com"
port = 80 # HTTP only
path = (
"/v1/forecast?"
"latitude={}&longitude={}"
"&current=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,windspeed"
"&current=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,wind_speed_10m,wind_gusts_10m"
"&forecast_hours=8"
"&hourly=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,wind_speed_10m,wind_gusts_10m"
"&forecast_days=10"
"&daily=temperature_2m_max,temperature_2m_min,dewpoint_2m_min,dewpoint_2m_max,pressure_msl_min,pressure_msl_max,precipitation_sum,weather_code,wind_speed_10m_max,wind_gusts_10m_max"
"&timezone=auto"
).format(self.lat, self.lon)
print("Weather fetch: ", path)
# Resolve DNS
addr = socket.getaddrinfo(host, port, socket.AF_INET)[0][-1]
print("DNS", addr)
s = socket.socket()
s.connect(addr)
# Send HTTP request
request = (
"GET {} HTTP/1.1\r\n"
"Host: {}\r\n"
"Connection: close\r\n\r\n"
).format(path, host)
s.send(request.encode())
# ---- Read response ----
# Skip HTTP headers
buffer = b""
while True:
chunk = s.recv(256)
if not chunk:
raise Exception("No response")
buffer += chunk
header_end = buffer.find(b"\r\n\r\n")
if header_end != -1:
body = buffer[header_end + 4:]
break
# Read remaining body
while True:
chunk = s.recv(512)
if not chunk:
break
body += chunk
s.close()
# Strip non-json parts
body = body[5:]
body = body[:-7]
print("Have result:", body.decode())
data = DownloadManager.download_url("https://"+host+path)
if not data:
self.summary = "Download error"
return
#print("Have result:", body.decode())
# Parse JSON
data = ujson.loads(body)
data = ujson.loads(data)
# ---- Extract data ----
print("\n\n")
s = ""
print("---- ")
cw = data["current"]
self.now = Hourly(cw)
self.summary = self.now.summarize()
self.now = Hourly()
self.now.init(cw, None)
prev = self.now
t = self.now.summarize()
s += t + "\n"
print(t)
self.hourly = []
d = data["hourly"]
times = d["time"]
#print(d)
print("---- ")
for i in range(len(times)):
h = Hourly()
h.init(d, i)
h.time = times[i]
self.hourly.append(h)
if not h.similar(prev):
t = h.summarize()
s += t + "\n"
print(t)
prev = h
self.daily = []
d = data["daily"]
times = d["time"]
#print(d)
print("---- ")
for i in range(len(times)):
h = Daily()
h.init(d, i)
h.time = times[i]
self.daily.append(h)
if i == 0:
prev = h
elif not h.similar(prev):
t = h.summarize()
s += t + "\n"
print(t)
prev = h
self.summary = s
def summarize_future():
now = utime.time()
# Rain detection in next 24h
for h in weather.hourly[:24]:
if h["precip"] >= 1.0:
return "Rain soon"
# Temperature trend
if len(weather.hourly) > 24:
t0 = weather.hourly[0]["temp"]
t24 = weather.hourly[24]["temp"]
if abs(t24 - t0) < 2:
return "No change expected"
if t24 > t0:
return "Getting warmer"
else:
return "Getting cooler"
return "Stable weather"
weather = Weather()
@@ -167,32 +266,38 @@ class Main(Activity):
# ---- MAIN SCREEN ----
label_time = lv.label(scr_main)
label_time.set_text("(time)")
label_time.align(lv.ALIGN.TOP_LEFT, 10, 40)
label_time.set_style_text_font(lv.font_montserrat_24, 0)
self.label_time = label_time
label_weather = lv.label(scr_main)
label_weather.set_text(f"Weather for {weather.name} ({weather.lat}, {weather.lon})")
label_weather.align_to(label_time, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10)
label_weather.set_text(f"{weather.name} ({weather.lat}, {weather.lon})")
label_weather.align(lv.ALIGN.TOP_LEFT, 10, 24)
label_weather.set_style_text_font(lv.font_montserrat_14, 0)
self.label_weather = label_weather
btn_hourly = lv.button(scr_main)
btn_hourly.align(lv.ALIGN.TOP_RIGHT, -5, 24)
lv.label(btn_hourly).set_text("Reload")
btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None)
label_time = lv.label(scr_main)
label_time.set_text("(time)")
label_time.align_to(btn_hourly, lv.ALIGN.TOP_LEFT, -85, -10)
label_time.set_style_text_font(lv.font_montserrat_24, 0)
self.label_time = label_time
label_summary = lv.label(scr_main)
label_summary.set_text("(weather)")
#label_summary.set_long_mode(lv.label.LONG.WRAP)
label_summary.set_width(300)
#label_summary.set_width(300)
label_summary.align_to(label_weather, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5)
label_summary.set_style_text_font(lv.font_montserrat_24, 0)
self.label_summary = label_summary
btn_hourly = lv.button(scr_main)
btn_hourly.set_size(100, 40)
btn_hourly.align(lv.ALIGN.BOTTOM_LEFT, 10, -10)
lv.label(btn_hourly).set_text("Reload")
btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None)
if False:
btn_daily = lv.button(scr_main)
btn_daily.set_size(100, 40)
btn_daily.align(lv.ALIGN.BOTTOM_RIGHT, -10, -10)
lv.label(btn_daily).set_text("Daily")
self.setContentView(self.screen)
@@ -223,3 +328,87 @@ class Main(Activity):
self.label_summary.set_text("Requesting...")
weather.fetch()
# --------------------
def code():
# -----------------------------
# LVGL UI
# -----------------------------
scr_main = lv.obj()
scr_hourly = lv.obj()
scr_daily = lv.obj()
# ---- HOURLY SCREEN ----
hourly_list = lv.list(scr_hourly)
hourly_list.set_size(320, 200)
hourly_list.align(lv.ALIGN.TOP_MID, 0, 10)
btn_back1 = lv.button(scr_hourly)
btn_back1.set_size(80, 30)
btn_back1.align(lv.ALIGN.BOTTOM_MID, 0, -5)
lv.label(btn_back1).set_text("Back")
# ---- DAILY SCREEN ----
daily_list = lv.list(scr_daily)
daily_list.set_size(320, 200)
daily_list.align(lv.ALIGN.TOP_MID, 0, 10)
btn_back2 = lv.button(scr_daily)
btn_back2.set_size(80, 30)
btn_back2.align(lv.ALIGN.BOTTOM_MID, 0, -5)
lv.label(btn_back2).set_text("Back")
def foo():
btn_hourly.add_event_cb(go_hourly, lv.EVENT.CLICKED, None)
btn_daily.add_event_cb(go_daily, lv.EVENT.CLICKED, None)
btn_back1.add_event_cb(go_back, lv.EVENT.CLICKED, None)
btn_back2.add_event_cb(go_back, lv.EVENT.CLICKED, None)
# -----------------------------
# STARTUP
# -----------------------------
def go_hourly(e):
populate_hourly()
lv.scr_load(scr_hourly)
def go_daily(e):
populate_daily()
lv.scr_load(scr_daily)
def go_back(e):
lv.scr_load(scr_main)
def update_ui():
if weather.current_temp is not None:
text = "%s %.1f C" % (
weather_code_to_text(weather.current_code),
weather.current_temp
)
label_weather.set_text(text)
label_summary.set_text(weather.summary)
def populate_hourly():
hourly_list.clean()
for h in weather.hourly[:24]:
line = "%s %.1fC %.1fmm" % (
h["time"][11:16],
h["temp"],
h["precip"]
)
hourly_list.add_text(line)
def populate_daily():
daily_list.clean()
for d in weather.daily:
line = "%s %.1f/%.1f" % (
d["date"],
d["high"],
d["low"]
)
daily_list.add_text(line)
@@ -0,0 +1,26 @@
import sys
from . import hx8357d
from . import _hx8357d_init
# Register _hx8357d_init in sys.modules so __import__('_hx8357d_init') can find it
# This is needed because display_driver_framework.py uses __import__('_hx8357d_init')
# expecting a top-level module, but _hx8357d_init is in the hx8357d package subdirectory
sys.modules['_hx8357d_init'] = _hx8357d_init
# Explicitly define __all__ and re-export public symbols from hx8357d module
__all__ = [
'HX8357D',
'STATE_HIGH',
'STATE_LOW',
'STATE_PWM',
'BYTE_ORDER_RGB',
'BYTE_ORDER_BGR',
]
# Re-export the public symbols
HX8357D = hx8357d.HX8357D
STATE_HIGH = hx8357d.STATE_HIGH
STATE_LOW = hx8357d.STATE_LOW
STATE_PWM = hx8357d.STATE_PWM
BYTE_ORDER_RGB = hx8357d.BYTE_ORDER_RGB
BYTE_ORDER_BGR = hx8357d.BYTE_ORDER_BGR
@@ -0,0 +1,93 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
import time
from micropython import const # NOQA
import lvgl as lv # NOQA
import lcd_bus # NOQA
_SWRESET = const(0x01)
_SLPOUT = const(0x11)
_DISPON = const(0x29)
_COLMOD = const(0x3A)
_MADCTL = const(0x36)
_TEON = const(0x35)
_TEARLINE = const(0x44)
_SETOSC = const(0xB0)
_SETPWR1 = const(0xB1)
_SETRGB = const(0xB3)
_SETCOM = const(0xB6)
_SETCYC = const(0xB4)
_SETC = const(0xB9)
_SETSTBA = const(0xC0)
_SETPANEL = const(0xCC)
_SETGAMMA = const(0xE0)
def init(self):
param_buf = bytearray(34)
param_mv = memoryview(param_buf)
time.sleep_ms(300) # NOQA
param_buf[:3] = bytearray([0xFF, 0x83, 0x57])
self.set_params(_SETC, param_mv[:3])
param_buf[0] = 0x80
self.set_params(_SETRGB, param_mv[:1])
param_buf[:4] = bytearray([0x00, 0x06, 0x06, 0x25])
self.set_params(_SETCOM, param_mv[:4])
param_buf[0] = 0x68
self.set_params(_SETOSC, param_mv[:1])
param_buf[0] = 0x05
self.set_params(_SETPANEL, param_mv[:1])
param_buf[:6] = bytearray([0x00, 0x15, 0x1C, 0x1C, 0x83, 0xAA])
self.set_params(_SETPWR1, param_mv[:6])
param_buf[:6] = bytearray([0x50, 0x50, 0x01, 0x3C, 0x1E, 0x08])
self.set_params(_SETSTBA, param_mv[:6])
param_buf[:7] = bytearray([0x02, 0x40, 0x00, 0x2A, 0x2A, 0x0D, 0x78])
self.set_params(_SETCYC, param_mv[:7])
param_buf[:34] = bytearray([
0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35, 0x41, 0x4b, 0x4b, 0x42, 0x3A,
0x27, 0x1B, 0x08, 0x09, 0x03, 0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35,
0x41, 0x4b, 0x4b, 0x42, 0x3A, 0x27, 0x1B, 0x08, 0x09, 0x03, 0x00, 0x01])
self.set_params(_SETGAMMA, param_mv[:34])
param_buf[0] = (
self._madctl(
self._color_byte_order,
self._ORIENTATION_TABLE # NOQA
)
)
self.set_params(_MADCTL, param_mv[:1])
color_size = lv.color_format_get_size(self._color_space)
if color_size == 2: # NOQA
pixel_format = 0x55
else:
raise RuntimeError(
f'{self.__class__.__name__} IC only supports '
'lv.COLOR_FORMAT.RGB565'
)
param_buf[0] = pixel_format
self.set_params(_COLMOD, param_mv[:1])
param_buf[0] = 0x00
self.set_params(_TEON, param_mv[:1])
param_buf[:2] = bytearray([0x00, 0x02])
self.set_params(_TEARLINE, param_mv[:2])
time.sleep_ms(150) # NOQA
self.set_params(_SLPOUT)
time.sleep_ms(50) # NOQA
self.set_params(_DISPON)
@@ -0,0 +1,15 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
import display_driver_framework
STATE_HIGH = display_driver_framework.STATE_HIGH
STATE_LOW = display_driver_framework.STATE_LOW
STATE_PWM = display_driver_framework.STATE_PWM
BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB
BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR
class HX8357D(display_driver_framework.DisplayDriver):
pass
@@ -0,0 +1,127 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
import lvgl as lv # NOQA
from micropython import const # NOQA
import micropython # NOQA
import machine # NOQA
import pointer_framework
import time
_CMD_X_READ = const(0xD0) # 12 bit resolution
_CMD_Y_READ = const(0x90) # 12 bit resolution
_CMD_Z1_READ = const(0xB0)
_CMD_Z2_READ = const(0xC0)
_MIN_RAW_COORD = const(10)
_MAX_RAW_COORD = const(4090)
class XPT2046(pointer_framework.PointerDriver):
touch_threshold = 400
confidence = 5
margin = 50
def __init__(
self,
device: machine.SPI.Bus,
display_width: int,
display_height: int,
lcd_cs: int,
touch_cs: int,
touch_cal=None,
startup_rotation=lv.DISPLAY_ROTATION._0,
debug=False,
):
self._device = device # machine.SPI.Bus() instance, shared with display
self._debug = debug
self.lcd_cs = machine.Pin(lcd_cs, machine.Pin.OUT, value=0)
self.touch_cs = machine.Pin(touch_cs, machine.Pin.OUT, value=1)
self._width = display_width
self._height = display_height
self._tx_buf = bytearray(3)
self._tx_mv = memoryview(self._tx_buf)
self._rx_buf = bytearray(3)
self._rx_mv = memoryview(self._rx_buf)
self.__confidence = max(min(self.confidence, 25), 3)
self.__points = [[0, 0] for _ in range(self.__confidence)]
margin = max(min(self.margin, 100), 1)
self.__margin = margin * margin
super().__init__(
touch_cal=touch_cal, startup_rotation=startup_rotation, debug=debug
)
def _read_reg(self, reg, num_bytes):
self._tx_buf[0] = reg
self._device.write_readinto(self._tx_mv[:num_bytes], self._rx_mv[:num_bytes])
return ((self._rx_buf[1] << 8) | self._rx_buf[2]) >> 3
def _get_coords(self):
try:
self.lcd_cs.value(1) # deselect LCD to avoid conflicts
self.touch_cs.value(0) # select touch chip
z1 = self._read_reg(_CMD_Z1_READ, 3)
z2 = self._read_reg(_CMD_Z2_READ, 3)
z = z1 + ((_MAX_RAW_COORD + 6) - z2)
if z < self.touch_threshold:
return None # Not touched
points = self.__points
count = 0
end_time = time.ticks_us() + 5000
while time.ticks_us() < end_time:
if count == self.__confidence:
break
raw_x = self._read_reg(_CMD_X_READ, 3)
if raw_x < _MIN_RAW_COORD:
continue
raw_y = self._read_reg(_CMD_Y_READ, 3)
if raw_y > _MAX_RAW_COORD:
continue
# put in buff
points[count][0] = raw_x
points[count][1] = raw_y
count += 1
finally:
self.touch_cs.value(1) # deselect touch chip
self.lcd_cs.value(0) # select LCD
if not count:
return None # Not touched
meanx = sum([points[i][0] for i in range(count)]) // count
meany = sum([points[i][1] for i in range(count)]) // count
dev = (
sum(
[
(points[i][0] - meanx) ** 2 + (points[i][1] - meany) ** 2
for i in range(count)
]
)
/ count
)
if dev >= self.__margin:
return None # Not touched
x = pointer_framework.remap(
meanx, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_width
)
y = pointer_framework.remap(
meany, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_height
)
if self._debug:
print(
f"{self.__class__.__name__}_TP_DATA({count=} {meanx=} {meany=} {z1=} {z2=} {z=})"
) # NOQA
return self.PRESSED, x, y

Some files were not shown because too many files have changed in this diff Show More