Files
Kivy_APP/main.py
2025-07-31 17:02:08 +08:00

740 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from kivy.properties import ObjectProperty, StringProperty, ListProperty
from datetime import datetime, timedelta
from kivy.core.text import LabelBase
from kivy.metrics import dp
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import Image
from kivymd.app import MDApp
from kivy.core.window import Window
from kivy.uix.screenmanager import ScreenManager
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.uix.datatables import MDDataTable
import random
import threading
import csv
from kivymd.uix.textfield import MDTextField
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.label import MDLabel
from kivy.uix.label import Label
from kivy.utils import platform
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle
from kivy_garden.graph import Graph, LinePlot
import modbus_tk
import modbus_tk.defines as cst
import socket
from modbus_client import ModbusClient
import re
from threading import Thread
from functools import partial
from collections import defaultdict
from random import randint
from kivymd.theming import ThemeManager
LabelBase.register(name="MPoppins", fn_regular="fonts/Chinese/msyh.ttf")
LabelBase.register(name="BPoppins", fn_regular="fonts/Chinese/msyh.ttf")
LabelBase.register(name="RRubik", fn_regular="fonts/Chinese/msyh.ttf")
LabelBase.register(name="RCro", fn_regular="fonts/Chinese/msyh.ttf")
LabelBase.register(name="RPac", fn_regular="fonts/Chinese/msyh.ttf")
class MainScreen(Screen):
pass
class HomeScreen(Screen):
home = ObjectProperty(None)
class LoginScreen(Screen):
login = ObjectProperty(None)
class ProfileScreen(Screen):
profile = ObjectProperty(None)
class ProfileEditScreen(Screen):
profile_edit = ObjectProperty(None)
class HistoryScreen(Screen):
history = ObjectProperty(None)
class ModifyCurrentParamScreen(Screen):
modify_current_param = ObjectProperty(None)
class ModifyVoltageParamScreen(Screen):
modify_voltage_param = ObjectProperty(None)
class ModifyLeakageParamScreen(Screen):
modify_leakage_param = ObjectProperty(None)
class ModifySystemParamScreen(Screen):
modify_system_param = ObjectProperty(None)
class ModifyProtectionParamScreen(Screen):
modify_protection_param = ObjectProperty(None)
class RealTimeCurveScreen(Screen):
real_time_curve = ObjectProperty(None)
class ControlCommandScreen(Screen):
control_command = ObjectProperty(None)
class AboutScreen(Screen):
about = ObjectProperty(None)
# Window.size = (dp(360), dp(680))
class app(MDApp):
wifi_status_text = StringProperty("")
def __init__(self):
super().__init__()
self.user_name = None
self.dialog = None
self.dialog2 = None
self.profile_edit_screen = None
self.edit_user_pass = None
self.edit_user_name = None
self.edit_wifi_ssid = None
self.edit_modbus_ip = None
self.edit_modbus_port = None
self.edit_nfc_id = None
self.edit_reserve = None
self.profile_user_pass = None
self.profile_user_name = None
self.profile_wifi_ssid = None
self.profile_modbus_ip = None
self.profile_modbus_port = None
self.profile_nfc_id = None
self.profile_reserve = None
self.user_pass = None
self.d1 = None
self.edit_semester = None
self.otp_button = None
self.new_pass = None
self.new_pass1 = None
self.mobile_no = None
self.password = None
self.username = None
self.modbus_master = ModbusClient() # Modbus连接对象
self.modbus_ip = None # Modbus服务器IP
self.modbus_port = None # Modbus服务器端口
self.theme_cls.font_styles["H1"] = ["BPoppins", 96, False, -1.5]
self.theme_cls.font_styles["H2"] = ["BPoppins", 60, False, -0.5]
self.theme_cls.font_styles["H3"] = ["BPoppins", 48, False, 0]
self.theme_cls.font_styles["H4"] = ["BPoppins", 34, False, 0.25]
self.theme_cls.font_styles["H5"] = ["BPoppins", 25, False, 0.15]
self.theme_cls.font_styles["H6"] = ["BPoppins", 16, False, 0.15]
self.theme_cls.font_styles["Subtitle1"] = ["BPoppins", 16, False, 0.15]
self.theme_cls.font_styles["Subtitle2"] = ["BPoppins", 14, False, 0.1]
self.theme_cls.font_styles["Body1"] = ["BPoppins", 16, False, 0.5]
self.theme_cls.font_styles["Body2"] = ["BPoppins", 14, False, 0.25]
self.theme_cls.font_styles["Button"] = ["BPoppins", 14, True, 1.25]
self.theme_cls.font_styles["Caption"] = ["BPoppins", 13, False, 0.15]
self.theme_cls.font_styles["Overline"] = ["BPoppins", 10, True, 1.5]
def on_text_field_focus(self, instance, value):
if value:
# 当MDTextField获得焦点时
# 获取ScrollView和MDList的引用
scroll_view = self.root.get_screen('modify_current_param').ids.real_time_scroll_view
md_list = self.root.get_screen('modify_current_param').ids.real_time_md_list
# 计算MDTextField在MDList中的相对位置
# instance.y 是MDTextField在父控件BoxLayout中的y坐标
# instance.parent.y 是BoxLayout在MDList中的y坐标
# 所以MDTextField在MDList中的y坐标是 instance.y + instance.parent.y
text_field_y_in_mdlist = instance.y + instance.parent.y
# 计算需要滚动的目标位置
# 目标是让MDTextField的底部与ScrollView的底部对齐或者至少在可见区域内
# scroll_view.height 是ScrollView的可见高度
# md_list.height 是MDList的总高度
# scroll_view.scroll_y 的范围是0到10表示底部1表示顶部
# 计算MDTextField相对于MDList顶部的距离
relative_y = md_list.height - text_field_y_in_mdlist - instance.height
# 将相对距离转换为scroll_y的值
# 确保不会滚动到超出范围
if md_list.height > scroll_view.height:
target_scroll_y = relative_y / (md_list.height - scroll_view.height)
# 限制target_scroll_y在0到1之间
target_scroll_y = max(0, min(1, target_scroll_y))
scroll_view.scroll_y = target_scroll_y
def build(self):
Builder.load_file('kv/app.kv')
screen_manager = ScreenManager()
screen_manager.add_widget(MainScreen(name="main"))
screen_manager.add_widget(HomeScreen(name="home"))
screen_manager.add_widget(LoginScreen(name="login"))
screen_manager.add_widget(HistoryScreen(name="history"))
screen_manager.add_widget(ProfileScreen(name="profile"))
screen_manager.add_widget(ProfileEditScreen(name="profile_edit"))
screen_manager.add_widget(ModifyCurrentParamScreen(name="modify_current_param"))
screen_manager.add_widget(ModifyVoltageParamScreen(name="modify_voltage_param"))
screen_manager.add_widget(ModifyLeakageParamScreen(name="modify_leakage_param"))
screen_manager.add_widget(ModifySystemParamScreen(name="modify_system_param"))
screen_manager.add_widget(ModifyProtectionParamScreen(name="modify_protection_param"))
screen_manager.add_widget(RealTimeCurveScreen(name="real_time_curve"))
screen_manager.add_widget(ControlCommandScreen(name="control_command"))
screen_manager.add_widget(AboutScreen(name="about"))
# 添加其他屏幕
return screen_manager
#############################################ALL INPUT TEXT############################################################
def on_start(self):
login_screen = self.root.get_screen("login")
home_screen = self.root.get_screen("home")
self.user = home_screen.ids.user
profile_screen = self.root.get_screen("profile")
self.profile_user_name = profile_screen.ids.profile_user_name
self.profile_user_pass = profile_screen.ids.profile_user_pass
self.profile_wifi_ssid = profile_screen.ids.profile_wifi_ssid
self.profile_modbus_ip = profile_screen.ids.profile_modbus_ip
self.profile_modbus_port = profile_screen.ids.profile_modbus_port
self.profile_nfc_id = profile_screen.ids.profile_nfc_id
self.profile_reserve = profile_screen.ids.profile_reserve
self.profile_edit_screen = self.root.get_screen("profile_edit")
self.edit_user_name = self.profile_edit_screen.ids.edit_user_name
self.edit_user_pass = self.profile_edit_screen.ids.edit_user_pass
self.edit_wifi_ssid = self.profile_edit_screen.ids.edit_wifi_ssid
self.edit_modbus_ip = self.profile_edit_screen.ids.edit_modbus_ip
self.edit_modbus_port = self.profile_edit_screen.ids.edit_modbus_port
self.edit_nfc_id = self.profile_edit_screen.ids.edit_nfc_id
self.edit_reserve = self.profile_edit_screen.ids.edit_reserve
self.root.bind(current=self.on_screen_changed)
self.register_update_event = None
self.wifi_update_event = None
def on_screen_changed(self, instance, value):
"""屏幕切换时启动/停止刷新"""
if value == "real_time_curve":
# 进入目标屏幕时启动定时刷新
if not self.register_update_event:
self.register_update_event = Clock.schedule_interval(self.update_register_display, 1)
else:
# 离开时停止刷新
if self.register_update_event:
self.register_update_event.cancel()
self.register_update_event = None
if value == "login":
if not self.wifi_update_event:
self.wifi_update_event = Clock.schedule_interval(self.update_wifi_status, 1)
else:
if self.wifi_update_event:
self.wifi_update_event.cancel()
self.wifi_update_event = None
#############################################@Frequently used functions##################################################
@staticmethod
def change_cursor(is_enter):
"""改变鼠标指针样式"""
# 如果is_enter为True鼠标进入目标区域将鼠标指针改为手型
if is_enter:
Window.set_system_cursor('hand')
# 否则(鼠标离开目标区域),将鼠标指针改回默认箭头样式
else:
Window.set_system_cursor('arrow')
@staticmethod
def toggle_password_visibility(password_field, icon_button):
"""切换密码输入框的可见性状态"""
# 检查密码输入框当前是否处于密码隐藏状态
if password_field.password:
# 如果是隐藏状态,切换为可见状态
password_field.password = False
# 更新图标为"eye"(眼睛图标,表示当前可见)
icon_button.icon = "eye"
else:
# 如果是可见状态,切换为隐藏状态(显示为圆点/星号)
password_field.password = True
# 更新图标为"eye-off"(闭眼图标,表示当前隐藏)
icon_button.icon = "eye-off"
def clear_text(self):
pass
def on_press_back_arrow(self):
pass
############################################@DIALOG BOX most used functions##############################################
# MDDialog box function 'dialog'
def dialog1(self, text, title="Tips"):
close_button = MDFlatButton(
text="关闭",
font_name="MPoppins",
on_release=self.close_dialog
)
content_label = Label(
text=text,
font_name="MPoppins",
color=(0, 0, 0, 1),
size_hint_y=None,
height=dp(30),
halign="center",
valign="middle"
)
content_label.bind(size=content_label.setter('text_size'))
self.dialog = MDDialog(
title=title,
type="custom",
content_cls=content_label,
size_hint=(0.84, None),
buttons=[close_button]
)
self.dialog.open()
def dialog2(self, title, text):
close_button = MDFlatButton(
text="关闭",
font_name="MPoppins",
on_release=self.close_dialog
)
content_label = Label(
text=text,
font_name="MPoppins",
color=(0, 0, 0, 1),
size_hint_y=None,
height=dp(30),
halign="center",
valign="middle"
)
content_label.bind(size=content_label.setter('text_size'))
self.dialog = MDDialog(
title=title,
type="custom",
content_cls=content_label,
size_hint=(0.84, None),
buttons=[close_button]
)
self.dialog.open()
# MDDialog box dismiss function 'close_dialog'
def close_dialog(self, *args):
self.dialog.dismiss()
#############################################Modbus Functions############################################################
def show_modify_dialog(self, label_id, current_value=""):
"""显示修改对话框"""
if not self.dialog2:
# 创建输入框
self.modify_input = MDTextField(
id="modify_input",
hint_text="请输入新值",
input_filter="int", # 保持整数输入限制
text=current_value
)
# 创建对话框
self.dialog2 = MDDialog(
title="修改参数",
type="custom",
content_cls=MDBoxLayout(
self.modify_input,
orientation="vertical",
padding=dp(10),
spacing=dp(10),
size_hint_y=None,
height=dp(100)
),
buttons=[
MDFlatButton(
text="取消",
on_press=lambda x: self.dialog2.dismiss()
),
MDFlatButton(
text="确认",
on_press=lambda x: self.confirm_modify(label_id)
)
]
)
else:
# 重置输入框内容
self.modify_input.text = current_value
self.dialog2.open()
def confirm_modify(self, label_id):
"""确认修改并更新标签,增强鲁棒性"""
try:
new_value = self.modify_input.text
if not new_value:
self.show_dialog("错误", "请输入新值")
return
screen = self.root.get_screen("real_time_curve")
label = screen.ids.get(label_id, None)
if label:
label.text = f"通信超时: {new_value}"
# 调用原有的修改方法,增加异常捕获
try:
self.modify_register(new_value)
except Exception as e:
self.show_dialog("错误", f"寄存器修改失败:{e}")
else:
self.show_dialog("错误", "未找到目标标签")
except Exception as e:
self.show_dialog("错误", f"操作异常:{e}")
finally:
if self.dialog2:
self.dialog2.dismiss()
def read_modbus_registers(self, slave_id=1, address=0, count=1):
"""
通用的Modbus保持寄存器读取函数
:param slave_id: 从机ID
:param address: 寄存器地址
:param count: 读取数量
:return: {'success': bool, 'data': list, 'msg': str}
"""
return self.modbus_master.read_holding_registers(slave_id, address, count)
def modify_register(self, field_widget, input_text):
"""
用户主动修改时,写入指定寄存器并刷新显示
:param field_widget: MDTextField控件对象
:param input_text: 用户输入的值
"""
address = getattr(field_widget, 'comm_address', None)
slave_id = 1 # 可根据实际情况动态获取
if address is None:
self.show_dialog("错误", "未设置通讯地址")
return
if not input_text:
self.show_dialog("错误", "请输入值")
return
try:
value = int(input_text)
result = self.write_modbus_register(slave_id=slave_id, address=address, value=value)
# self.show_dialog("操作结果", result)
self.update_register_display(0)
except ValueError:
self.show_dialog("错误", "请输入有效整数")
def write_modbus_register(self, slave_id=1, address=0, value=None):
"""
通用写入Modbus保持寄存器(单个寄存器)
:param slave_id: 从机ID
:param address: 寄存器地址
:param value: 要写入的值(整数,0-65535)
:return: {'success': bool, 'msg': str}
"""
return self.modbus_master.write_single_register(slave_id, address, value)
def show_dialog(self, title, message):
self.dialog1(message,title)
def update_register_display(self, dt):
real_time_screen = self.root.get_screen("real_time_curve")
widgets = [w for w in real_time_screen.ids.values()
if isinstance(w, MDTextField) and hasattr(w, 'comm_address')]
# 收集未聚焦的寄存器地址
addr_widget_map = {}
for w in widgets:
if not w.focus:
addr = getattr(w, 'comm_address', None)
if addr is not None:
addr_widget_map[addr] = w
if not addr_widget_map:
return
# 分组连续地址段(例如 [1,2,3,10,11,12] -> [[1,2,3], [10,11,12]])
sorted_addresses = sorted(addr_widget_map.keys())
grouped_ranges = []
group = [sorted_addresses[0]]
for addr in sorted_addresses[1:]:
if addr == group[-1] + 1:
group.append(addr)
else:
grouped_ranges.append(group)
group = [addr]
grouped_ranges.append(group)
# 开启线程异步读取每段
for group in grouped_ranges:
start = group[0]
count = group[-1] - start + 1
thread = Thread(target=self._read_and_update_range, args=(start, count, group, addr_widget_map))
thread.start()
def _read_and_update_range(self, start, count, group, addr_widget_map):
try:
result = self.read_modbus_registers(slave_id=1, address=start, count=count)
except Exception as e:
result = {'success': False, 'error': str(e)}
# 在主线程中更新 UI
Clock.schedule_once(partial(self._update_widgets_with_data, start, group, addr_widget_map, result), 0)
def _update_widgets_with_data(self, start, group, addr_widget_map, result, dt):
if result.get('success') and result.get('data'):
data = result['data']
for addr in group:
widget = addr_widget_map.get(addr)
index = addr - start
if widget and 0 <= index < len(data):
new_value = str(data[index])
if widget.text != new_value:
widget.text = new_value
elif widget:
widget.text = "索引错误"
else:
for addr in group:
widget = addr_widget_map.get(addr)
if widget:
widget.text = f"失败: {result.get('error', '读取失败')}"
###################################LoginPageWork-Start#################################################
def update_wifi_status(self, dt):
# 只有当前屏幕为 login 时才更新
if self.root.current != "login":
return
if platform != 'android':
self.wifi_status_text = '非Android设备'
return
try:
from jnius import autoclass, cast
PythonActivity = autoclass('org.kivy.android.PythonActivity')
Context = autoclass('android.content.Context')
activity = PythonActivity.mActivity
WifiManager = autoclass('android.net.wifi.WifiManager')
Formatter = autoclass('android.text.format.Formatter')
wifi_service = activity.getSystemService(Context.WIFI_SERVICE)
wifi_manager = cast('android.net.wifi.WifiManager', wifi_service)
if not wifi_manager.isWifiEnabled():
self.wifi_status_text = 'WiFi 未启用'
return
wifi_info = wifi_manager.getConnectionInfo()
if wifi_info is None:
self.wifi_status_text = 'WiFi 信息不可用'
return
ssid = wifi_info.getSSID()
ssid_display = ssid.strip('"') if ssid else "SSID Unknown"
ip_int = wifi_info.getIpAddress()
ip_str = Formatter.formatIpAddress(ip_int) if ip_int != 0 else "0.0.0.0"
link_speed = wifi_info.getLinkSpeed() # Mbps
bssid = wifi_info.getBSSID()
rssi = wifi_info.getRssi()
self.wifi_status_text = (
f'SSID: {ssid_display}\n'
f'IP: {ip_str}\n'
f'速度: {link_speed} Mbps\n'
f'BSSID: {bssid}\n'
f'信号: {rssi} dBm'
)
except Exception as e:
self.wifi_status_text = f'获取WiFi信息失败\n{e}'
def verify(self, obj=None):
try:
if platform == "android":
from jnius import autoclass
PythonActivity = autoclass('org.kivy.android.PythonActivity')
Context = autoclass('android.content.Context')
activity = PythonActivity.mActivity
wifi_service = activity.getSystemService(Context.WIFI_SERVICE)
wifi_info = wifi_service.getConnectionInfo()
# 获取当前连接的WiFi名称并处理引号和大小写
wifi_id = wifi_info.getSSID().strip('"').lower()
else:
# 非Android平台使用模拟WiFi
wifi_id = "zhizhan-2"
except Exception as e:
self.dialog1(f"获取WiFi信息失败:{e}")
return
try:
with open("data/Users.csv", "r", encoding="utf-8") as file:
# 使用DictReader读取CSV通过列名访问数据
csv_reader = csv.DictReader(file)
for row in csv_reader:
# 获取CSV中存储的WiFi SSID并处理大小写
csv_ssid = row['Wifi_SSID'].strip().lower()
# 精确匹配WiFi SSID
if wifi_id == csv_ssid:
# 通过列名获取用户信息
name = row['User']
password = row['User_pass']
wifi_ssid = row['Wifi_SSID']
modbus_ip = row['Modbus_IP']
modbus_port = row['Modbus_Port']
nfc_id = row['NFC_ID']
reserve = row['Reserve']
# 更新界面显示的用户信息
self.root.current = "home"
self.user.text = f"[b]Hey! {name}[/b]"
self.profile_user_name.text = self.edit_user_name.text = name
self.profile_user_pass.text = self.edit_user_pass.text = password
self.profile_wifi_ssid.text = self.edit_wifi_ssid.text = wifi_ssid
self.profile_modbus_ip.text = self.edit_modbus_ip.text = modbus_ip
self.profile_modbus_port.text = self.edit_modbus_port.text = modbus_port
self.profile_nfc_id.text = self.edit_nfc_id.text = nfc_id
self.profile_reserve.text = self.edit_reserve.text = reserve
# 连接Modbus设备
self.connect_modbus()
self.dialog1(f"欢迎你,{name}!\n认证成功!")
return
except Exception as e:
self.dialog1(f"读取用户信息失败:{e}")
return
# 认证失败提示
self.dialog1("认证失败,请检查网络或确保手机权限打开定位和连接到目标WiFi")
def connect_modbus(self):
def _connect_in_background():
modbus_ip = None
modbus_port = 502
master = None
try:
# 获取当前WiFi SSID
if platform == "android":
from jnius import autoclass
PythonActivity = autoclass('org.kivy.android.PythonActivity')
Context = autoclass('android.content.Context')
activity = PythonActivity.mActivity
wifi_service = activity.getSystemService(Context.WIFI_SERVICE)
wifi_info = wifi_service.getConnectionInfo()
current_wifi_id = wifi_info.getSSID().strip('"').lower()
else:
current_wifi_id = "zhizhan-2"
# 读取CSV配置
with open("data/Users.csv", "r", encoding="utf-8") as file:
csv_reader = csv.DictReader(file)
for row in csv_reader:
csv_ssid = row['Wifi_SSID'].strip().lower()
if current_wifi_id == csv_ssid:
modbus_ip = row['Modbus_IP']
modbus_port = int(row['Modbus_Port']) if row['Modbus_Port'] else 502
break
if not modbus_ip:
Clock.schedule_once(lambda dt: self.dialog1("未找到匹配的Modbus配置"))
return
# 断开现有连接
self.modbus_master.disconnect()
# 创建新连接并测试
connect_result = self.modbus_master.connect(modbus_ip, modbus_port)
if not connect_result['success']:
Clock.schedule_once(lambda dt: self.dialog1(connect_result['msg']))
return
print(f"Modbus连接成功\nIP: {modbus_ip}\n端口: {modbus_port}")
# Clock.schedule_once(lambda dt: self.dialog1(f"Modbus连接成功\nIP: {modbus_ip}\n端口: {modbus_port}"))
except FileNotFoundError as e:
# 修复:将异常信息转为字符串或通过默认参数传递
err_msg = "配置文件 Users.csv 未找到"
Clock.schedule_once(lambda dt, msg=err_msg: self.dialog1(msg))
except modbus_tk.modbus.ModbusError as e:
# 修复:使用默认参数传递异常信息
err_msg = f"Modbus协议错误: {e}\n从站地址: {e.slave}\n功能码: {e.function_code}"
Clock.schedule_once(lambda dt, msg=err_msg: self.dialog1(msg))
self.modbus_master = None
except socket.error as e:
err_msg = f"网络连接失败: {e}"
Clock.schedule_once(lambda dt, msg=err_msg: self.dialog1(msg))
self.modbus_master = None
except Exception as e:
# 修复:捕获所有其他异常
err_msg = f"连接失败: {str(e)}"
Clock.schedule_once(lambda dt, msg=err_msg: self.dialog1(msg))
self.modbus_master = None
threading.Thread(target=_connect_in_background, daemon=True).start()
def edit_profile(self):
if self.edit_user_name.text == "" or self.edit_user_pass.text == "" or self.edit_wifi_ssid.text == "" or self.edit_modbus_ip.text == "" or self.edit_modbus_port.text == "" or self.edit_nfc_id.text == "" or self.edit_reserve.text == "":
check = "Enter all required fields"
return self.dialog1(check)
else:
user_data = {
'User': self.edit_user_name.text,
'User_pass': self.edit_user_pass.text,
'Wifi_SSID': self.edit_wifi_ssid.text,
'Modbus_IP': self.edit_modbus_ip.text,
'Modbus_Port': self.edit_modbus_port.text,
'NFC_ID': self.edit_nfc_id.text,
'Reserve': self.edit_reserve.text
}
update_user_profile(user_data, self.profile_wifi_ssid.text)
self.verify(True)
self.root.current = "home"
def secure_profile(self):
self.profile_edit_screen.ids.edit_user_pass.readonly = True
self.profile_edit_screen.ids.edit_wifi_ssid.readonly = True
self.profile_edit_screen.ids.edit_modbus_ip.readonly = True
self.profile_edit_screen.ids.edit_modbus_port.readonly = True
self.profile_edit_screen.ids.edit_nfc_id.readonly = True
self.profile_edit_screen.ids.edit_reserve.readonly = False
from user_data_manager import update_user_profile
if __name__ == '__main__':
app().run()