diff --git a/.gitignore b/.gitignore index ec273bf..4b1f593 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ data/tag.txt data/idmasa.txt # Offline wheel cache (large, OS-specific) -Files/reposytory/ +Files/repository/ Files/system_packages/ # Misc diff --git a/Files/reposytory/FreeSimpleGUI-5.0.0-py3-none-any.whl b/Files/reposytory/FreeSimpleGUI-5.0.0-py3-none-any.whl deleted file mode 100644 index 1d8d809..0000000 Binary files a/Files/reposytory/FreeSimpleGUI-5.0.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/Harting/HA_MWG_new_cert.crt b/Files/reposytory/Harting/HA_MWG_new_cert.crt deleted file mode 100644 index b19507d..0000000 --- a/Files/reposytory/Harting/HA_MWG_new_cert.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDqjCCApKgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBiDEpMCcGA1UECgwgSEFS -VElORyBJVCBTZXJ2aWNlcyBHbWJIICBDby4gS0cxDjAMBgNVBAsMBUlULUlTMRIw -EAYDVQQHDAlFc3BlbGthbXAxDDAKBgNVBAgMA05SVzELMAkGA1UEBhMCREUxHDAa -BgNVBAMME0hBUlRJTkcgV2ViIEdhdGV3YXkwHhcNMTYxMjIxMTU0NzA0WhcNMzYx -MjIxMTU0NzA0WjCBiDEpMCcGA1UECgwgSEFSVElORyBJVCBTZXJ2aWNlcyBHbWJI -ICBDby4gS0cxDjAMBgNVBAsMBUlULUlTMRIwEAYDVQQHDAlFc3BlbGthbXAxDDAK -BgNVBAgMA05SVzELMAkGA1UEBhMCREUxHDAaBgNVBAMME0hBUlRJTkcgV2ViIEdh -dGV3YXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCm6bFPeLYge36x -WAsif9qMKEyJtrgAhtAbRWgmMWZW3s2TUMkL9Ovi5M9jc+W0V5aGCS8ueul5o9z2 -acTV9FT5DDBRXGEEMITpzGh8iRPZsFrNStk8L9oEXzjj87OE+n/gvnCe7KALzsUR -UQ+FDKLGQjMzHNTwUtadRZwm+MAP02l0izTQF7ny3zpRdco8faLvSVITkX9a6N5E -N6HfftOeoQ9xMolwielanaEbcoUKJlmV7cmAw0BjezE8CT5J0MEcaGqw6ek3pCk5 -Glt7jk+JE4X3UdUB9M60P+6pgZEmpbeoRXQqIKqrS8vC0cn0RiBPcyeoBXy0sqvK -Aun+2m+DAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG -SIb3DQEBCwUAA4IBAQBarWNg89Li73TnwQly0Pn6n+/xa4P9wfDjueotoBqFY1BH -CWNPxQxgZoDoNrbj0HXbRPGs0KAXp0KJGES/SkgFwtByH1e2eCM3BWr9FfLJvU+e -AuGPHtqBHMxXxFEWpk2PMVconP1G3wlfU36Gd/Ujnac0wDqjkkYrVJzSNjhWJ73X -4TOLKLDyP01Q0I1mbKT6LNPmG/kBt0zWa513tt+PaU1fCejzFM1FBng2KPhmKoJn -1fNZHJVUpVf0vo9Mf3QztMvpVuZ9uicK/B5qHIrfvCB/XD8hDmRfrvZF0cQMSp0D -3JYs2emjeYBaH0phQtDfRJ8OUMe15DRBa7dy75hP ------END CERTIFICATE----- diff --git a/Files/reposytory/Harting/HartingIssuingCA1.crt b/Files/reposytory/Harting/HartingIssuingCA1.crt deleted file mode 100644 index 1dfb372..0000000 --- a/Files/reposytory/Harting/HartingIssuingCA1.crt +++ /dev/null @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIHBzCCBO+gAwIBAgITQwAAAAkJewLoz5nxpgADAAAACTANBgkqhkiG9w0BAQsF -ADBKMRUwEwYKCZImiZPyLGQBGRYFSW50cmExFzAVBgoJkiaJk/IsZAEZFgdIQVJU -SU5HMRgwFgYDVQQDEw9IQVJUSU5HIFJvb3QgQ0EwHhcNMTYwMTIyMTI1MjAxWhcN -MjQwMTIyMTMwMjAxWjBOMRUwEwYKCZImiZPyLGQBGRYFaW50cmExFzAVBgoJkiaJ -k/IsZAEZFgdoYXJ0aW5nMRwwGgYDVQQDExNIQVJUSU5HIElzc3VpbmcgQ0ExMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAutoC7Q/AmRR+I/s7YRsotka7 -kgCfSkRINNeIut+0gv4GchmztbuLraIz7RpQbBW4pRPWJT4J5fVzBN6HSuGv6VBB -H72MHD3cTAbPpCeAvMHQVYCt+nfM/5w4WnOfYpjSIcV3rf6d/cOWKESpQs81F3xz -TNbITWP3SiCeRuxdL4U+1LvE1vj2O2mqG5kkXR51XWzgC4oOh2aR9y1TY48AmAva -WOGBbAKz8QAV8dnW7kq+r9UmRxRisSVsLbWasiTfwndYacBQQK3Iw8YyJK0DbMqS -rBbY5kkRoF48t4JuR3T7VGlGqhKlDg9AxxOw1HkNtBB3sQw05tUZhPtOxtyORQID -AQABo4IC4DCCAtwwEgYJKwYBBAGCNxUBBAUCAwMAAzAjBgkrBgEEAYI3FQIEFgQU -bxiNOHg8qzcCUYJasvRJYn+CpnUwHQYDVR0OBBYEFNUJktkZLIVxRG2f4lr9qFWs -7SM5MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFGetmY4Lmx9ZU/ng6WF5pRSGvFyJMIIB -CwYDVR0fBIIBAjCB/zCB/KCB+aCB9oYyaHR0cDovL3d3dy5oYXJ0aW5nLmNvbS9w -a2kvSEFSVElORyUyMFJvb3QlMjBDQS5jcmyGgb9sZGFwOi8vL0NOPUhBUlRJTkcl -MjBSb290JTIwQ0EsQ049Y2FoYXJ0aW5nMSxDTj1DRFAsQ049UHVibGljJTIwS2V5 -JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1oYXJ0 -aW5nLERDPWludHJhP2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3Q/YmFzZT9vYmpl -Y3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2ludDCCARcGCCsGAQUFBwEBBIIBCTCC -AQUwTAYIKwYBBQUHMAKGQGh0dHA6Ly93d3cuaGFydGluZy5jb20vcGtpL2NhaGFy -dGluZzFfSEFSVElORyUyMFJvb3QlMjBDQSgzKS5jcnQwgbQGCCsGAQUFBzAChoGn -bGRhcDovLy9DTj1IQVJUSU5HJTIwUm9vdCUyMENBLENOPUFJQSxDTj1QdWJsaWMl -MjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERD -PWhhcnRpbmcsREM9aW50cmE/Y0FDZXJ0aWZpY2F0ZT9iYXNlP29iamVjdENsYXNz -PWNlcnRpZmljYXRpb25BdXRob3JpdHkwDQYJKoZIhvcNAQELBQADggIBAGGWPYKM -Kzh7KKiNgMKbPJH25JV201GDMOUEwmxW1umfbgrAtbHWfpCfGzbk6A8ekK/9A6yk -fCj35WvJyB1L6lGbaAOJjC4loI9XQsbQokGJ+8SsUiVFABx63idXKVqBrbtpnj0L -XY8RQikTbvvXZNHBmMlo6dZ6FDn+xHHKSTuISf88UTJE/X6beAiMe4m3gT4loiN1 -u9L987TvW6FBbstRFGjWaXX7dQA1JzyQaoM4QJNGU2ZCTdF+8oOYy7abwqHr66f+ -bHkTd0qXMA50T5hbQmgRc5usgHUR4RtcTFu636YDeS0fi/x/lwe1UygTcEBSQA5U -TpJwjE3lX7aupdgF+g9hNBoFCI0HGnhzZDv4k1ugZYK533NleeKThmSWPmssxExb -siuly872stkotB/9sCEb5jbT2QnjPCIyxhGeFmHnDpPXrYlhcfgqpgA81NDFJ2Bz -McPf2qP+/uiPjbqbX6CG1cTF6QGuGdbSkLaH5cj2Pn9i9Mqzkb7qSB09wNiSI4B/ -/GzGKnal+1vIC78Y42FOU+b9WBegDLuNuWliQSZRcRGCj/CcegUwuGzOWf0Mc2SG -I1A5UI4Q8arg1RAXggqSOjYNAtdgl6s8wRpwzv55BHAkxvCe2/0r7p62zSfsvM5k -OIhrHDBIkA5bND00QUuVNf6pMb4sjN0d4cna ------END CERTIFICATE----- diff --git a/Files/reposytory/Harting/HartingRootCA.crt b/Files/reposytory/Harting/HartingRootCA.crt deleted file mode 100644 index 1ea4508..0000000 --- a/Files/reposytory/Harting/HartingRootCA.crt +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFljCCA36gAwIBAgIQWDrwWap0fIVAtVmbM/HtJzANBgkqhkiG9w0BAQsFADBK -MRUwEwYKCZImiZPyLGQBGRYFSW50cmExFzAVBgoJkiaJk/IsZAEZFgdIQVJUSU5H -MRgwFgYDVQQDEw9IQVJUSU5HIFJvb3QgQ0EwIBcNMDUwMzMwMDk0NjI0WhgPMjA2 -MjEyMTIxMjE5MTBaMEoxFTATBgoJkiaJk/IsZAEZFgVJbnRyYTEXMBUGCgmSJomT -8ixkARkWB0hBUlRJTkcxGDAWBgNVBAMTD0hBUlRJTkcgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBANd3pkQAWf4/WV3220uv4O4utXMmBeUf -V90oVJMf07GQUaScfc+vUzgK80fzcDjmA4Hde2Ag+f8Dj/Q+ZLAukIVms6qmfOUC -p5dIkMca5ept5WRoOTu+opxylbC1cfEvesbPV7x82QTnIfzQGNTppH6SlRttPk2M -aYaKjOCFhvmnQa6IHqYOTHyk8RrLnBdXe378SEseNn9bJmySnW9agLWwL2L4d1Lz -DuXAC6EVAxkkTJKoQYu0WQaKLh7cdzDEuI4k8PiFatQUFRiQT6tbc6d7D1vE7+DT -SjU8YizU3m9Bs7kkHsSQ85EPQ1nZ6aQz9VC41Yjh3lxst3LscWj0unvur8rqeQow -UOVwFKERB+BB1jSCcHV136Ij0CzLRXSIYcMFAsHTLHscc5g6nrYfDPRU/RlhuSkV -9mpeKkv8XYa0N3yCVy3Fr0NSXNehaONs26v1tMheojsvOFDC93MCN3NuYHcPQRt7 -MsLf1tuKTPUiagmCJLRbMaUCF5Prq2YI9kvE9crt3DvW75RFN2yO52Cjn4aLMacv -T+tmjbSQWzMZVOHTlEREsLcniOo4gGH1D/ijrJl1t/a8WiTxpv/Ub0S56NP1uZ1s -nukaQ5w/WeZk2mAe76pDUltGuZVk0opYsyVdbj30XlzSzNJOgAJ9UabkFjwwyIDx -AMHdwPrrPLzNAgMBAAGjdjB0MAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/ -MB0GA1UdDgQWBBRnrZmOC5sfWVP54OlheaUUhrxciTAQBgkrBgEEAYI3FQEEAwIB -AzAjBgkrBgEEAYI3FQIEFgQU5EC6gNFM7jw9fXMRJa1eANrFWSYwDQYJKoZIhvcN -AQELBQADggIBAAFMdOlGwtolH9fqtPy6AvkmSVeQ1S5bcDYCfXFTX0MyctDZSAeq -qsvVoYsxh34eePMzYpTRjCFJ9fFwe3zFO34eDkInxbdLUTMxBhc1wf8Q57UTYB3v -rKnB0DWIfrvVV989iVU1Cb/WzixcWf5tQayifEf2mFQvPhAPATu5IxXhyVPGkuoO -3c1JRZvfxKXEq8lzwZz20yUMaWpu+uM8mFHQ1XKMz3DvKmvJDdvRuvjU5VfyED6W -a5s0Qv9vtpekb05Bw5f6V6HJabi/Pws3tImZdQi7XRsDvQSjOvGnsyp73xsfzkj4 -JCsvXuf8M6k7rznPsE3iUm5rBLbTlb1WfTScs4PDIAxQK9o8QglQ/eD07VnHsgg9 -fBPWXu7NFrT+y2QDbvDE/Mjq03l0KiAATY1IsD2kE7upKAyKiS/jF+dDyBEMqj6T -5yRVbyoI6vS4wINx21I7XsC3EzfaUs13QgGAIIgJKVYrGJwEq9ARBpqmKpyKAJwt -0UkTqGZjBYTOW8/hUNpJd07M4087PGVEUNbPBp6yrddpuVllGR3n8p296KcR6m+d -cSkKAdreXI0Cpb+KmJpOuQj1rOTRxYAnYEE4zR2nd2JptjDLcxOLgMP0naZ6g8qS -LarqnBFK6cVlpeNNg+A7Ns96PBSJoYlBd4EYqfD20he1mZVnCrmjOFZh ------END CERTIFICATE----- diff --git a/Files/reposytory/LinuxPOS-Driver/CUPS driver guide V128.pdf b/Files/reposytory/LinuxPOS-Driver/CUPS driver guide V128.pdf deleted file mode 100644 index a8bb9a3..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/CUPS driver guide V128.pdf and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/Readme_POS-CUPS.txt b/Files/reposytory/LinuxPOS-Driver/Readme_POS-CUPS.txt deleted file mode 100644 index 30bd8c9..0000000 --- a/Files/reposytory/LinuxPOS-Driver/Readme_POS-CUPS.txt +++ /dev/null @@ -1,21 +0,0 @@ -Linux CUPS drivers for POS printer - -The following CUPS drivers are included here. - -ctzpos-cups_1.x.x-x_i386.deb - Debian (32 bit) -ctzpos-cups_1.x.x-x_amd64.deb - Debian (64 bit) -ctzpos-cups_1.x.x-x_armhf.deb - Raspberry Pi OS (32bit) -ctzpos-cups_1.x.x-x_arm64.deb - Raspberry Pi OS (64bit) - -ctzpos-cups-1.x.x-x.i586.rpm - Red Hat (32bit) -ctzpos-cups-1.x.x-x.x86_64.rpm - Red Hat (64bit) - -ctzpos-cups-1.x.x-x.src.rpm - CUPS driver source code in rpm format - -"deb" file is for Debian and it can be used on Ubuntu etc. -"rpm" file is for Red Hat and it can be used on CentOS, Fedora etc. - -Source code is inlucded to use CUPS driver on other Linux distribution. -Basic explanation for driver compile and driver installation is on following site. -http://www.citizen-systems.co.jp/en/printer/download/driver-cups-source - diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.i586.rpm b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.i586.rpm deleted file mode 100644 index 83ebbd4..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.i586.rpm and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.src.rpm b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.src.rpm deleted file mode 100644 index 19f93a6..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.src.rpm and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.x86_64.rpm b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.x86_64.rpm deleted file mode 100644 index eff63b1..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups-1.2.8-0.x86_64.rpm and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_amd64.deb b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_amd64.deb deleted file mode 100644 index f68a316..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_amd64.deb and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_arm64.deb b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_arm64.deb deleted file mode 100644 index eb161b6..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_arm64.deb and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_i386.deb b/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_i386.deb deleted file mode 100644 index bda8601..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos-cups_1.2.8-0_i386.deb and /dev/null differ diff --git a/Files/reposytory/LinuxPOS-Driver/ctzpos.deb b/Files/reposytory/LinuxPOS-Driver/ctzpos.deb deleted file mode 100644 index 29506e2..0000000 Binary files a/Files/reposytory/LinuxPOS-Driver/ctzpos.deb and /dev/null differ diff --git a/Files/reposytory/PyAutoGUI-0.9.54-py3-none-any.whl b/Files/reposytory/PyAutoGUI-0.9.54-py3-none-any.whl deleted file mode 100644 index f887f85..0000000 Binary files a/Files/reposytory/PyAutoGUI-0.9.54-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/PySocks-1.7.1-py3-none-any.whl b/Files/reposytory/PySocks-1.7.1-py3-none-any.whl deleted file mode 100644 index ac66f59..0000000 Binary files a/Files/reposytory/PySocks-1.7.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/WebPrintClient/wcpp.deb b/Files/reposytory/WebPrintClient/wcpp.deb deleted file mode 100644 index 79b78ca..0000000 Binary files a/Files/reposytory/WebPrintClient/wcpp.deb and /dev/null differ diff --git a/Files/reposytory/aiohappyeyeballs-2.6.1-py3-none-any.whl b/Files/reposytory/aiohappyeyeballs-2.6.1-py3-none-any.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl b/Files/reposytory/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/aiosignal-1.3.2-py2.py3-none-any.whl b/Files/reposytory/aiosignal-1.3.2-py2.py3-none-any.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/attrs-25.1.0-py3-none-any.whl b/Files/reposytory/attrs-25.1.0-py3-none-any.whl deleted file mode 100644 index 64aa913..0000000 Binary files a/Files/reposytory/attrs-25.1.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/attrs-25.3.0-py3-none-any.whl b/Files/reposytory/attrs-25.3.0-py3-none-any.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/certifi-2025.1.31-py3-none-any.whl b/Files/reposytory/certifi-2025.1.31-py3-none-any.whl deleted file mode 100644 index 7e488e8..0000000 Binary files a/Files/reposytory/certifi-2025.1.31-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/charset_normalizer-3.4.1-py3-none-any.whl b/Files/reposytory/charset_normalizer-3.4.1-py3-none-any.whl deleted file mode 100644 index 9faf0eb..0000000 Binary files a/Files/reposytory/charset_normalizer-3.4.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/evdev-1.7.1-cp311-cp311-linux_armv7l.whl b/Files/reposytory/evdev-1.7.1-cp311-cp311-linux_armv7l.whl deleted file mode 100644 index 342c221..0000000 Binary files a/Files/reposytory/evdev-1.7.1-cp311-cp311-linux_armv7l.whl and /dev/null differ diff --git a/Files/reposytory/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl b/Files/reposytory/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/func_timeout-4.3.5-py3-none-any.whl b/Files/reposytory/func_timeout-4.3.5-py3-none-any.whl deleted file mode 100644 index 2aa25cd..0000000 Binary files a/Files/reposytory/func_timeout-4.3.5-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/h11-0.14.0-py3-none-any.whl b/Files/reposytory/h11-0.14.0-py3-none-any.whl deleted file mode 100644 index 6ac1ef3..0000000 Binary files a/Files/reposytory/h11-0.14.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/idna-3.10-py3-none-any.whl b/Files/reposytory/idna-3.10-py3-none-any.whl deleted file mode 100644 index 52759bd..0000000 Binary files a/Files/reposytory/idna-3.10-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl b/Files/reposytory/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/outcome-1.3.0.post0-py2.py3-none-any.whl b/Files/reposytory/outcome-1.3.0.post0-py2.py3-none-any.whl deleted file mode 100644 index bf2277f..0000000 Binary files a/Files/reposytory/outcome-1.3.0.post0-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/packaging-24.2-py3-none-any.whl b/Files/reposytory/packaging-24.2-py3-none-any.whl deleted file mode 100644 index 5d67c58..0000000 Binary files a/Files/reposytory/packaging-24.2-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl b/Files/reposytory/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl deleted file mode 100644 index e69de29..0000000 diff --git a/Files/reposytory/pynput-1.7.6-py2.py3-none-any.whl b/Files/reposytory/pynput-1.7.6-py2.py3-none-any.whl deleted file mode 100644 index 1375d49..0000000 Binary files a/Files/reposytory/pynput-1.7.6-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/pyserial-3.5-py2.py3-none-any.whl b/Files/reposytory/pyserial-3.5-py2.py3-none-any.whl deleted file mode 100644 index 7751288..0000000 Binary files a/Files/reposytory/pyserial-3.5-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/python3-aiohttp-dbgsym_3.8.4-1_armhf.ddeb b/Files/reposytory/python3-aiohttp-dbgsym_3.8.4-1_armhf.ddeb deleted file mode 100644 index c8562d2..0000000 Binary files a/Files/reposytory/python3-aiohttp-dbgsym_3.8.4-1_armhf.ddeb and /dev/null differ diff --git a/Files/reposytory/python3-aiohttp.deb b/Files/reposytory/python3-aiohttp.deb deleted file mode 100644 index 59a274b..0000000 Binary files a/Files/reposytory/python3-aiohttp.deb and /dev/null differ diff --git a/Files/reposytory/python3-aiohttp_3.8.4-1_armhf.deb b/Files/reposytory/python3-aiohttp_3.8.4-1_armhf.deb deleted file mode 100644 index 59a274b..0000000 Binary files a/Files/reposytory/python3-aiohttp_3.8.4-1_armhf.deb and /dev/null differ diff --git a/Files/reposytory/python3_xlib-0.15-py3-none-any.whl b/Files/reposytory/python3_xlib-0.15-py3-none-any.whl deleted file mode 100644 index 46d3e99..0000000 Binary files a/Files/reposytory/python3_xlib-0.15-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/python_dotenv-1.0.1-py3-none-any.whl b/Files/reposytory/python_dotenv-1.0.1-py3-none-any.whl deleted file mode 100644 index 170a0e8..0000000 Binary files a/Files/reposytory/python_dotenv-1.0.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/python_xlib-0.31-py2.py3-none-any.whl b/Files/reposytory/python_xlib-0.31-py2.py3-none-any.whl deleted file mode 100644 index 40d82cc..0000000 Binary files a/Files/reposytory/python_xlib-0.31-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/rdm6300-0.1.1-py3-none-any.whl b/Files/reposytory/rdm6300-0.1.1-py3-none-any.whl deleted file mode 100644 index 4aebddf..0000000 Binary files a/Files/reposytory/rdm6300-0.1.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/requests-2.32.3-py3-none-any.whl b/Files/reposytory/requests-2.32.3-py3-none-any.whl deleted file mode 100644 index 0888cee..0000000 Binary files a/Files/reposytory/requests-2.32.3-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/selenium-4.29.0-cp311-cp311-linux_armv7l.whl b/Files/reposytory/selenium-4.29.0-cp311-cp311-linux_armv7l.whl deleted file mode 100644 index 45b2cb6..0000000 Binary files a/Files/reposytory/selenium-4.29.0-cp311-cp311-linux_armv7l.whl and /dev/null differ diff --git a/Files/reposytory/six-1.16.0-py2.py3-none-any.whl b/Files/reposytory/six-1.16.0-py2.py3-none-any.whl deleted file mode 100644 index fd94265..0000000 Binary files a/Files/reposytory/six-1.16.0-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/sniffio-1.3.1-py3-none-any.whl b/Files/reposytory/sniffio-1.3.1-py3-none-any.whl deleted file mode 100644 index 48f37d4..0000000 Binary files a/Files/reposytory/sniffio-1.3.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/sortedcontainers-2.4.0-py2.py3-none-any.whl b/Files/reposytory/sortedcontainers-2.4.0-py2.py3-none-any.whl deleted file mode 100644 index 61f63d3..0000000 Binary files a/Files/reposytory/sortedcontainers-2.4.0-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/trio-0.29.0-py3-none-any.whl b/Files/reposytory/trio-0.29.0-py3-none-any.whl deleted file mode 100644 index 7efa9b4..0000000 Binary files a/Files/reposytory/trio-0.29.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/trio_websocket-0.12.1-py3-none-any.whl b/Files/reposytory/trio_websocket-0.12.1-py3-none-any.whl deleted file mode 100644 index 2d3dfb9..0000000 Binary files a/Files/reposytory/trio_websocket-0.12.1-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/typing_extensions-4.11.0-py3-none-any.whl b/Files/reposytory/typing_extensions-4.11.0-py3-none-any.whl deleted file mode 100644 index fc1635c..0000000 Binary files a/Files/reposytory/typing_extensions-4.11.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/typing_extensions-4.12.2-py3-none-any.whl b/Files/reposytory/typing_extensions-4.12.2-py3-none-any.whl deleted file mode 100644 index f6cc799..0000000 Binary files a/Files/reposytory/typing_extensions-4.12.2-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/urllib3-2.3.0-py3-none-any.whl b/Files/reposytory/urllib3-2.3.0-py3-none-any.whl deleted file mode 100644 index e78504f..0000000 Binary files a/Files/reposytory/urllib3-2.3.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/webdriver_manager-4.0.2-py2.py3-none-any.whl b/Files/reposytory/webdriver_manager-4.0.2-py2.py3-none-any.whl deleted file mode 100644 index 0430e4a..0000000 Binary files a/Files/reposytory/webdriver_manager-4.0.2-py2.py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/websocket_client-1.8.0-py3-none-any.whl b/Files/reposytory/websocket_client-1.8.0-py3-none-any.whl deleted file mode 100644 index 6026aa3..0000000 Binary files a/Files/reposytory/websocket_client-1.8.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/wsproto-1.2.0-py3-none-any.whl b/Files/reposytory/wsproto-1.2.0-py3-none-any.whl deleted file mode 100644 index e1580e8..0000000 Binary files a/Files/reposytory/wsproto-1.2.0-py3-none-any.whl and /dev/null differ diff --git a/Files/reposytory/yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl b/Files/reposytory/yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl deleted file mode 100644 index e69de29..0000000 diff --git a/app.py b/app.py index eb53e5a..d4a2a70 100644 --- a/app.py +++ b/app.py @@ -14,24 +14,35 @@ CONFIGURATION_MODE = False # Run dependency check before importing anything else try: + # ABI-specific wheels are available for both Bookworm (cp311) and Trixie (cp313). + # pip will pick the correct file for the running interpreter automatically. + _pyver = f"cp{sys.version_info.major}{sys.version_info.minor}" + _abi_wheels = { + 'aiohttp': f'aiohttp-3.13.5-{_pyver}-{_pyver}-linux_armv7l.whl', + 'frozenlist': f'frozenlist-1.8.0-{_pyver}-{_pyver}-linux_armv7l.whl', + 'multidict': f'multidict-6.7.1-{_pyver}-{_pyver}-linux_armv7l.whl', + 'propcache': f'propcache-0.5.2-{_pyver}-{_pyver}-linux_armv7l.whl', + 'yarl': f'yarl-1.24.2-{_pyver}-{_pyver}-linux_armv7l.whl', + } check_and_install_dependencies({ - 'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl', - 'gpiozero': None, - 'requests': 'requests-2.32.3-py3-none-any.whl', - 'aiohttp': 'aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', - 'flask': None, - 'urllib3': 'urllib3-2.3.0-py3-none-any.whl', - 'certifi': 'certifi-2025.1.31-py3-none-any.whl', + 'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl', + 'serial': 'pyserial-3.5-py2.py3-none-any.whl', + 'gpiozero': None, + 'requests': 'requests-2.32.3-py3-none-any.whl', + 'aiohttp': _abi_wheels['aiohttp'], + 'flask': None, + 'urllib3': 'urllib3-2.3.0-py3-none-any.whl', + 'certifi': 'certifi-2025.1.31-py3-none-any.whl', 'charset_normalizer': 'charset_normalizer-3.4.1-py3-none-any.whl', - 'idna': 'idna-3.10-py3-none-any.whl', - 'multidict': 'multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', - 'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl', - 'frozenlist': 'frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', - 'attrs': 'attrs-25.3.0-py3-none-any.whl', - 'yarl': 'yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', - 'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl', - 'propcache': 'propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl' - }, "./Files/reposytory") + 'idna': 'idna-3.10-py3-none-any.whl', + 'multidict': _abi_wheels['multidict'], + 'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl', + 'frozenlist': _abi_wheels['frozenlist'], + 'attrs': 'attrs-25.3.0-py3-none-any.whl', + 'yarl': _abi_wheels['yarl'], + 'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl', + 'propcache': _abi_wheels['propcache'], + }, "./Files/repository") except Exception as e: print(f"Warning: Dependency check failed: {e}") print("Continuing with existing packages...") @@ -103,32 +114,6 @@ except ImportError as e: print("Async functionality may be limited") import asyncio -# Import Flask for command server -try: - from flask import Flask, request, jsonify - print("✓ Flask imported successfully") - FLASK_AVAILABLE = True -except ImportError as e: - print(f"Warning: Could not import Flask: {e}") - print("Command server functionality will be disabled") - FLASK_AVAILABLE = False - # Create dummy Flask classes - class Flask: - def __init__(self, name): - pass - def route(self, *args, **kwargs): - def decorator(f): - return f - return decorator - def run(self, *args, **kwargs): - pass - - def request(): - pass - - def jsonify(data): - return data - import configparser import json @@ -646,58 +631,8 @@ def check_system_requirements(): """ print("Checking system requirements...") - # 1. Check and install required system packages - system_packages = { - 'sshpass': 'sshpass_1.09-1_armhf.deb' # Required for auto-update functionality - } - - for package, deb_file in system_packages.items(): - try: - result = subprocess.run(['which', package], capture_output=True, text=True) - if result.returncode == 0: - print(f"✓ {package} is installed") - else: - print(f"Installing {package}...") - - # Try online installation first - try: - install_result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True, text=True, timeout=120) - install_result = subprocess.run(['sudo', 'apt', 'install', '-y', package], - capture_output=True, text=True, timeout=300) - if install_result.returncode == 0: - print(f"✓ {package} installed successfully (online)") - continue - else: - print(f"Online installation failed, trying offline...") - except Exception as online_error: - print(f"Online installation failed: {online_error}, trying offline...") - - # Try offline installation from local .deb file - deb_path = f"./Files/system_packages/{deb_file}" - if os.path.exists(deb_path): - try: - print(f"Installing {package} from local package: {deb_path}") - offline_result = subprocess.run(['sudo', 'dpkg', '-i', deb_path], - capture_output=True, text=True, timeout=120) - if offline_result.returncode == 0: - print(f"✓ {package} installed successfully (offline)") - else: - print(f"✗ Offline installation failed: {offline_result.stderr}") - # Try to fix dependencies - print("Attempting to fix dependencies...") - subprocess.run(['sudo', 'apt', '--fix-broken', 'install', '-y'], - capture_output=True, text=True, timeout=300) - except Exception as offline_error: - print(f"✗ Offline installation error: {offline_error}") - else: - print(f"✗ Local package not found: {deb_path}") - print(f" To add offline support, download with: apt download {package}") - - except Exception as e: - print(f"Warning: Could not check/install {package}: {e}") - - # 2. Check and create required directories - required_dirs = ['./data', './Files', './Files/reposytory', './Files/system_packages'] + # 1. Check and create required directories + required_dirs = ['./data', './Files', './Files/repository', './Files/system_packages'] for dir_path in required_dirs: try: os.makedirs(dir_path, exist_ok=True) @@ -736,65 +671,6 @@ def check_system_requirements(): return True -def check_port_capabilities(): - """ - Check if the application can bind to port 80 and set up capabilities if needed - """ - print("Checking port 80 capabilities...") - - try: - # Check if we're running as root - if os.geteuid() == 0: - print("✓ Running as root - port 80 access available") - return True - - # Check if capabilities are set - python_path = sys.executable - result = subprocess.run(['getcap', python_path], capture_output=True, text=True) - - if 'cap_net_bind_service=ep' in result.stdout: - print("✓ Port binding capabilities already set") - return True - - # Try to set capabilities - print("Setting up port 80 binding capabilities...") - setup_script = './setup_port_capability.sh' - - if os.path.exists(setup_script): - result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) - if result.returncode == 0: - print("✓ Port capabilities set successfully") - return True - else: - print(f"✗ Failed to set capabilities: {result.stderr}") - else: - # Create the setup script if it doesn't exist - script_content = f'''#!/bin/bash -# Set port binding capability for Python to allow port 80 access -echo "Setting port binding capability for Python..." -sudo setcap cap_net_bind_service=ep {python_path} -echo "Capability set successfully" -''' - try: - with open(setup_script, 'w') as f: - f.write(script_content) - os.chmod(setup_script, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH) - - result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) - if result.returncode == 0: - print("✓ Port capabilities set successfully") - return True - else: - print(f"✗ Failed to set capabilities: {result.stderr}") - except Exception as e: - print(f"✗ Failed to create setup script: {e}") - - except Exception as e: - print(f"Warning: Could not check port capabilities: {e}") - - print("Warning: Port 80 may not be accessible. App will try to run on default port.") - return False - def check_hardware_interfaces(): """ Check hardware interfaces (UART/Serial) required for RFID reader @@ -920,7 +796,6 @@ def perform_system_initialization(): initialization_steps = [ ("System Requirements", check_system_requirements), - ("Port Capabilities", check_port_capabilities), ("Hardware Interfaces", check_hardware_interfaces), ("GPIO Permissions", initialize_gpio_permissions), ("Network Connectivity", check_network_connectivity) @@ -1125,269 +1000,6 @@ def log_info_with_server(message): # CONFIGURATION_MODE not defined yet (during initialization) send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server -# Function to execute system commands with proper security -def execute_system_command(command): - """ - Execute system commands with proper logging and security checks - """ - # Define allowed commands for security - allowed_commands = [ - "sudo apt update", - "sudo apt upgrade -y", - "sudo apt update && sudo apt upgrade -y", # Combined update and upgrade - "sudo apt autoremove -y", - "sudo apt autoclean", - "sudo reboot", - "sudo shutdown -h now", - "df -h", - "free -m", - "uptime", - "systemctl status", - "sudo systemctl restart networking", - "sudo systemctl restart ssh" - ] - - try: - # Check if command is allowed - if command not in allowed_commands: - log_info_with_server(f"Command '{command}' is not allowed for security reasons") - return {"status": "error", "message": f"Command '{command}' is not allowed", "output": ""} - - log_info_with_server(f"Executing command: {command}") - - # Execute the command - result = subprocess.run( - command.split(), - capture_output=True, - text=True, - timeout=300 # 5 minute timeout - ) - - output = result.stdout + result.stderr - - if result.returncode == 0: - log_info_with_server(f"Command '{command}' executed successfully") - return {"status": "success", "message": "Command executed successfully", "output": output} - else: - log_info_with_server(f"Command '{command}' failed with return code {result.returncode}") - return {"status": "error", "message": f"Command failed with return code {result.returncode}", "output": output} - - except subprocess.TimeoutExpired: - log_info_with_server(f"Command '{command}' timed out") - return {"status": "error", "message": "Command timed out", "output": ""} - except Exception as e: - log_info_with_server(f"Error executing command '{command}': {str(e)}") - return {"status": "error", "message": f"Error: {str(e)}", "output": ""} - -# Flask app for receiving commands (only if Flask is available) -if FLASK_AVAILABLE: - command_app = Flask(__name__) - - @command_app.route('/execute_command', methods=['POST']) - def handle_command_execution(): - """ - Endpoint to receive and execute system commands - """ - try: - data = request.json - if not data or 'command' not in data: - return jsonify({"error": "Invalid request. 'command' field is required"}), 400 - - command = data.get('command') - - # Execute the command - result = execute_system_command(command) - - return jsonify(result), 200 if result['status'] == 'success' else 400 - - except Exception as e: - log_info_with_server(f"Error handling command execution request: {str(e)}") - return jsonify({"error": f"Server error: {str(e)}"}), 500 - - @command_app.route('/status', methods=['GET']) - def get_device_status(): - """ - Endpoint to get device status information - """ - try: - n_masa = read_name_from_file() - - # Get system information - uptime_result = subprocess.run(['uptime'], capture_output=True, text=True) - df_result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True) - free_result = subprocess.run(['free', '-m'], capture_output=True, text=True) - - status_info = { - "hostname": hostname, - "device_ip": device_ip, - "nume_masa": n_masa, - "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - "uptime": uptime_result.stdout.strip() if uptime_result.returncode == 0 else "N/A", - "disk_usage": df_result.stdout.strip() if df_result.returncode == 0 else "N/A", - "memory_usage": free_result.stdout.strip() if free_result.returncode == 0 else "N/A" - } - - return jsonify(status_info), 200 - - except Exception as e: - log_info_with_server(f"Error getting device status: {str(e)}") - return jsonify({"error": f"Error getting status: {str(e)}"}), 500 - - @command_app.route('/auto_update', methods=['POST']) - def auto_update_app(): - """ - Trigger an immediate WMT client update check against the monitoring server. - Delegates to _check_and_apply_update(). - """ - result = _check_and_apply_update() - if result.get('updated'): - return jsonify({"status": "success", "message": result['message']}), 200 - elif result.get('error'): - return jsonify({"error": result['message']}), 500 - else: - return jsonify({"status": "no_update", "message": result['message']}), 200 - - @command_app.route('/update_config', methods=['POST']) - def update_config_endpoint(): - """ - Update configuration from Server_Monitorizare_v2. - Accepts a JSON body with sections matching config.txt structure. - Example body: {"chrome": {"chrome_url": "http://..."}} - """ - global APP_CONFIG - - ALLOWED_SECTIONS = { - "chrome": ["chrome_url", "chrome_local_url", "chrome_insecure_origin"], - "card_api": ["base_url"], - "server": ["log_url", "update_host", "update_user", "internet_check_host"], - "device": ["name", "hostname", "ip"], - } - - try: - data = request.json - if not data: - return jsonify({"error": "JSON body required"}), 400 - - config_path = "./data/config.txt" - parser = configparser.ConfigParser() - if os.path.exists(config_path): - parser.read(config_path) - - updated_keys = [] - for section, allowed_keys in ALLOWED_SECTIONS.items(): - if section in data and isinstance(data[section], dict): - if not parser.has_section(section): - parser.add_section(section) - for key in allowed_keys: - if key in data[section]: - parser.set(section, key, str(data[section][key])) - updated_keys.append(f"{section}.{key}") - - os.makedirs("./data", exist_ok=True) - with open(config_path, "w") as f: - f.write("# WMT Application Configuration\n") - f.write(f"# Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") - parser.write(f) - - APP_CONFIG = load_config() - log_info_with_server(f"Configuration updated via API: {updated_keys}") - return jsonify({"status": "success", "updated_keys": updated_keys}), 200 - - except Exception as e: - log_info_with_server(f"Error updating config: {str(e)}") - return jsonify({"error": f"Failed to update configuration: {str(e)}"}), 500 - - @command_app.route('/reload_config', methods=['POST']) - def reload_config_endpoint(): - """ - Reload configuration from data/config.txt into memory without restarting. - """ - global APP_CONFIG - try: - APP_CONFIG = load_config() - log_info_with_server("Configuration reloaded via API") - return jsonify({ - "status": "success", - "message": "Configuration reloaded", - "chrome_url": APP_CONFIG.get("chrome_url"), - "card_post_base_url": APP_CONFIG.get("card_post_base_url"), - "server_log_url": APP_CONFIG.get("server_log_url"), - "device_name": APP_CONFIG.get("work_place"), - }), 200 - except Exception as e: - return jsonify({"error": f"Failed to reload config: {str(e)}"}), 500 - - def start_command_server(): - """ - Start the Flask server with enhanced port handling and fallback - """ - # Try different ports in order of preference - preferred_ports = [ - int(os.environ.get('FLASK_PORT', 80)), # Use environment variable or default to 80 - 80, # Standard HTTP port - 5000, # Flask default - 8080, # Alternative HTTP port - 3000 # Development port - ] - - for port in preferred_ports: - try: - print(f"Attempting to start command server on port {port}...") - - # Test if port is available - import socket - test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - test_socket.bind(('0.0.0.0', port)) - test_socket.close() - - # Port is available, start Flask server - print(f"Port {port} is available. Starting command server...") - command_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False) - return # Success, exit function - - except PermissionError: - print(f"✗ Permission denied for port {port}") - if port == 80: - print(" Hint: Port 80 requires root privileges or capabilities") - print(" Try running: sudo setcap cap_net_bind_service=ep $(which python3)") - continue - except OSError as e: - if "Address already in use" in str(e): - print(f"✗ Port {port} is already in use") - else: - print(f"✗ Port {port} error: {e}") - continue - except Exception as e: - print(f"✗ Failed to start on port {port}: {e}") - continue - - # If we get here, all ports failed - log_info_with_server("Error: Could not start command server on any port") - print("✗ Could not start command server on any available port") - - # Start command server in a separate process with enhanced error handling - try: - print("Initializing command server...") - command_server_process = Process(target=start_command_server) - command_server_process.daemon = True # Ensure it dies with main process - command_server_process.start() - - # Give the server a moment to start and check if it's running - import time - time.sleep(2) - - if command_server_process.is_alive(): - port = int(os.environ.get('FLASK_PORT', 80)) - print(f"✓ Command server started successfully on port {port}") - else: - print("Warning: Command server process stopped unexpectedly") - - except Exception as e: - print(f"Warning: Could not start command server: {e}") - log_info_with_server(f"Command server startup error: {str(e)}") -else: - print("Warning: Flask not available - Command server disabled") # Call the function to delete old logs delete_old_logs() def config(): @@ -1482,110 +1094,6 @@ if not CONFIGURATION_MODE: else: print("🔧 Configuration mode: Internet connectivity monitoring DISABLED") -# --------------------------------------------------------------------------- -# HTTP-based client auto-update helpers -# --------------------------------------------------------------------------- - -def _get_local_version(): - """Return local app version as float by reading the first line of this file.""" - try: - with open(os.path.abspath(__file__), 'r') as f: - first_line = f.readline() - m = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE) - if m: - return float(m.group(1)) - except Exception: - pass - return None - - -def _check_and_apply_update(): - """ - Query the monitoring server for the latest WMT release. - If a newer version is available, download the zip, back up app.py, - extract into the WMT directory, and schedule a systemd service restart. - Returns a dict: {updated, message, error}. - """ - server_host = APP_CONFIG.get("server_host", "") - server_port = APP_CONFIG.get("server_port", "5000") - if not server_host: - return {"updated": False, "error": False, "message": "server_host not configured – skipping update check"} - - base_url = f"http://{server_host}:{server_port}" - local_version = _get_local_version() - - try: - resp = requests.get(f"{base_url}/api/wmt/client/version", timeout=10) - if resp.status_code != 200: - return {"updated": False, "error": True, "message": f"Version endpoint returned {resp.status_code}"} - meta = resp.json() - server_version = float(meta.get("version", 0)) - except Exception as e: - return {"updated": False, "error": True, "message": f"Could not reach version endpoint: {e}"} - - if local_version is not None and server_version <= local_version: - return {"updated": False, "error": False, "message": f"Already on latest version {local_version}"} - - log_info_with_server(f"WMT update available: local={local_version} server={server_version} – downloading …") - - wmt_dir = os.path.dirname(os.path.abspath(__file__)) - tmp_zip = "/tmp/wmt_update.zip" - app_py = os.path.join(wmt_dir, "app.py") - - try: - # Download the zip - dl = requests.get(f"{base_url}/api/wmt/client/download", timeout=60, stream=True) - if dl.status_code != 200: - return {"updated": False, "error": True, "message": f"Download endpoint returned {dl.status_code}"} - with open(tmp_zip, 'wb') as f: - for chunk in dl.iter_content(chunk_size=65536): - if chunk: - f.write(chunk) - - # Validate the zip - import zipfile - if not zipfile.is_zipfile(tmp_zip): - return {"updated": False, "error": True, "message": "Downloaded file is not a valid zip"} - - # Backup current app.py - bak = f"{app_py}.bak.{local_version or 'old'}" - try: - import shutil - shutil.copy2(app_py, bak) - except Exception as e: - log_info_with_server(f"Warning: could not back up app.py: {e}") - - # Extract into WMT directory – skip anything inside data/ to preserve device config - with zipfile.ZipFile(tmp_zip, 'r') as zf: - for member in zf.infolist(): - # Normalise path separators and skip the data folder - member_path = member.filename.replace('\\', '/') - if member_path.startswith('data/') or member_path == 'data': - continue - zf.extract(member, wmt_dir) - - log_info_with_server(f"WMT updated to version {server_version} – scheduling service restart") - - # Schedule restart via systemd (non-blocking) - subprocess.Popen( - ["bash", "-c", "sleep 3 && sudo systemctl restart wmt"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, start_new_session=True - ) - - return {"updated": True, "error": False, - "message": f"Updated from {local_version} to {server_version}. Restarting service …"} - - except Exception as e: - log_info_with_server(f"WMT auto-update failed: {e}") - return {"updated": False, "error": True, "message": str(e)} - finally: - try: - os.remove(tmp_zip) - except OSError: - pass - - # --------------------------------------------------------------------------- # Periodic server config sync (background thread) # --------------------------------------------------------------------------- @@ -1619,19 +1127,10 @@ def _periodic_config_sync(): except Exception as e: print(f"Periodic config sync error: {e}") - # Check for WMT client update every cycle (non-fatal) - try: - _check_and_apply_update() - except Exception as e: - print(f"Periodic update check error: {e}") - if not CONFIGURATION_MODE: _sync_thread = threading.Thread(target=_periodic_config_sync, daemon=True, name="config-sync") _sync_thread.start() print("✅ Periodic server config sync started (every 5 min)") - # Startup version check (non-blocking) - _update_thread = threading.Thread(target=_check_and_apply_update, daemon=True, name="startup-update-check") - _update_thread.start() else: print("🔧 Configuration mode: Periodic config sync DISABLED") diff --git a/app.py.bak.2.9 b/app.py.bak.2.9 new file mode 100644 index 0000000..eb53e5a --- /dev/null +++ b/app.py.bak.2.9 @@ -0,0 +1,1843 @@ +#App version 2.9 - Added configuration mode for "notconfig" devices +import os +import sys +import subprocess +import stat +import pwd +import grp +# Import dependency check/install functions from dependency_utils +from dependency_utils import install_package_from_wheel, check_and_install_dependencies + + +# Global configuration mode flag +CONFIGURATION_MODE = False + +# Run dependency check before importing anything else +try: + check_and_install_dependencies({ + 'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl', + 'gpiozero': None, + 'requests': 'requests-2.32.3-py3-none-any.whl', + 'aiohttp': 'aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', + 'flask': None, + 'urllib3': 'urllib3-2.3.0-py3-none-any.whl', + 'certifi': 'certifi-2025.1.31-py3-none-any.whl', + 'charset_normalizer': 'charset_normalizer-3.4.1-py3-none-any.whl', + 'idna': 'idna-3.10-py3-none-any.whl', + 'multidict': 'multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', + 'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl', + 'frozenlist': 'frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', + 'attrs': 'attrs-25.3.0-py3-none-any.whl', + 'yarl': 'yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl', + 'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl', + 'propcache': 'propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl' + }, "./Files/reposytory") +except Exception as e: + print(f"Warning: Dependency check failed: {e}") + print("Continuing with existing packages...") + +def safe_import(module_name, package_name=None): + """ + Safely import a module with error handling + """ + try: + if package_name: + module = __import__(package_name) + return getattr(module, module_name) + else: + return __import__(module_name) + except ImportError as e: + print(f"Warning: Could not import {module_name}: {e}") + return None + +# Now import required modules with fallbacks +rdm6300 = safe_import('rdm6300') +if rdm6300 is None: + print("ERROR: rdm6300 is required for this application to work!") + print("Please ensure rdm6300 is installed from the repository.") + sys.exit(1) + +# Import other required modules +import time +import logging + +# Try to import GPIO-related modules +try: + from gpiozero import OutputDevice + print("✓ gpiozero imported successfully") +except ImportError as e: + print(f"Warning: Could not import gpiozero: {e}") + print("LED functionality will be disabled") + # Create a dummy OutputDevice class + class OutputDevice: + def __init__(self, pin): + self.pin = pin + def on(self): + print(f"LED {self.pin} would turn ON") + def off(self): + print(f"LED {self.pin} would turn OFF") + +from multiprocessing import Process + +# Import network-related modules +try: + import requests + print("✓ requests imported successfully") +except ImportError as e: + print(f"ERROR: requests is required: {e}") + sys.exit(1) + +import threading +import urllib.parse +from datetime import datetime, timedelta +import socket +import signal + +# Import async modules +try: + import aiohttp + import asyncio + print("✓ aiohttp and asyncio imported successfully") +except ImportError as e: + print(f"Warning: Could not import aiohttp: {e}") + print("Async functionality may be limited") + import asyncio + +# Import Flask for command server +try: + from flask import Flask, request, jsonify + print("✓ Flask imported successfully") + FLASK_AVAILABLE = True +except ImportError as e: + print(f"Warning: Could not import Flask: {e}") + print("Command server functionality will be disabled") + FLASK_AVAILABLE = False + # Create dummy Flask classes + class Flask: + def __init__(self, name): + pass + def route(self, *args, **kwargs): + def decorator(f): + return f + return decorator + def run(self, *args, **kwargs): + pass + + def request(): + pass + + def jsonify(data): + return data + +import configparser +import json + + +def load_config(): + """ + Load application configuration from data/config.txt (INI format). + Falls back to legacy file (device_info.txt) and hardcoded defaults. + """ + config_path = "./data/config.txt" + defaults = { + "chrome_url": "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production", + "chrome_local_url": "", + "chrome_insecure_origin": "http://filesibiusb05.sibiusb.harting.intra", + "card_post_base_url": "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record", + "server_log_url": "http://rpi-ansible:80/logs", + "update_host": "rpi-ansible", + "update_user": "pi", + "internet_check_host": "filesibiusb05.sibiusb.harting.intra", + "work_place": "notconfig", + "device_hostname": "unknown-device", + "device_ip": "127.0.0.1", + "device_location": "", + "device_info_reviewed_at": "1970-01-01T00:00:00", + "card_presence": "enable", + "last_synced": "1970-01-01T00:00:00", + } + + cfg = dict(defaults) + parser = configparser.ConfigParser() + + if os.path.exists(config_path): + try: + parser.read(config_path) + + if parser.has_section("chrome"): + if parser.has_option("chrome", "chrome_url"): + cfg["chrome_url"] = parser.get("chrome", "chrome_url") + if parser.has_option("chrome", "chrome_local_url"): + cfg["chrome_local_url"] = parser.get("chrome", "chrome_local_url") + if parser.has_option("chrome", "chrome_insecure_origin"): + cfg["chrome_insecure_origin"] = parser.get("chrome", "chrome_insecure_origin") + + if parser.has_section("card_api"): + if parser.has_option("card_api", "base_url"): + cfg["card_post_base_url"] = parser.get("card_api", "base_url") + + if parser.has_section("server"): + if parser.has_option("server", "log_url"): + cfg["server_log_url"] = parser.get("server", "log_url") + if parser.has_option("server", "update_host"): + cfg["update_host"] = parser.get("server", "update_host") + if parser.has_option("server", "update_user"): + cfg["update_user"] = parser.get("server", "update_user") + if parser.has_option("server", "internet_check_host"): + cfg["internet_check_host"] = parser.get("server", "internet_check_host") + + if parser.has_section("device"): + if parser.has_option("device", "work_place"): + cfg["work_place"] = parser.get("device", "work_place") + elif parser.has_option("device", "name"): + cfg["work_place"] = parser.get("device", "name") + if parser.has_option("device", "hostname"): + cfg["device_hostname"] = parser.get("device", "hostname") + if parser.has_option("device", "ip"): + cfg["device_ip"] = parser.get("device", "ip") + if parser.has_option("device", "info_reviewed_at"): + cfg["device_info_reviewed_at"] = parser.get("device", "info_reviewed_at") + if parser.has_option("device", "location"): + cfg["device_location"] = parser.get("device", "location") + if parser.has_option("device", "card_presence"): + cfg["card_presence"] = parser.get("device", "card_presence") + + if parser.has_section("meta"): + if parser.has_option("meta", "last_synced"): + cfg["last_synced"] = parser.get("meta", "last_synced") + + print(f"\u2713 Configuration loaded from {config_path}") + except Exception as e: + print(f"Warning: Could not parse {config_path}: {e}. Using defaults.") + else: + # Fall back to legacy individual files + try: + with open("./data/device_info.txt", "r") as f: + lines = f.read().strip().split('\n') + if len(lines) >= 1 and lines[0].strip(): + cfg["device_hostname"] = lines[0].strip() + if len(lines) >= 2 and lines[1].strip(): + cfg["device_ip"] = lines[1].strip() + except Exception: + pass + print("config.txt not found - using legacy files and defaults") + + return cfg + + +# --------------------------------------------------------------------------- +# Server config sync (runs once at startup) +# --------------------------------------------------------------------------- + +def _get_mac_address(): + """Return the MAC address of the primary network interface.""" + for iface in ['eth0', 'wlan0', 'eth1']: + mac_path = f'/sys/class/net/{iface}/address' + try: + if os.path.exists(mac_path): + with open(mac_path) as f: + mac = f.read().strip() + if mac and mac != '00:00:00:00:00:00': + return mac + except Exception: + continue + # Fallback: derive from UUID node + import uuid as _uuid + raw = _uuid.getnode() + return ':'.join(f'{(raw >> i) & 0xff:02x}' for i in range(40, -1, -8)) + + +def _write_config_from_server(new_cfg): + """Overwrite data/config.txt with config received from the server.""" + import configparser as _cp + p = _cp.ConfigParser() + + p.add_section("chrome") + p.set("chrome", "chrome_url", new_cfg.get("chrome_url", "")) + p.set("chrome", "chrome_local_url", new_cfg.get("chrome_local_url", "")) + p.set("chrome", "chrome_insecure_origin", new_cfg.get("chrome_insecure_origin", "")) + + p.add_section("card_api") + p.set("card_api", "base_url", new_cfg.get("card_api_base_url", "")) + + p.add_section("server") + p.set("server", "log_url", new_cfg.get("server_log_url", "")) + p.set("server", "update_host", new_cfg.get("update_host", "")) + p.set("server", "update_user", new_cfg.get("update_user", "")) + p.set("server", "internet_check_host", new_cfg.get("internet_check_host", "")) + + p.add_section("device") + # Only overwrite work_place if the server provides a non-empty value. + # This prevents a freshly migrated device from losing its idmasa-sourced + # work_place on first startup when the server doesn't know the device yet. + server_device_name = new_cfg.get("device_name", "").strip() + local_work_place = APP_CONFIG.get("work_place", "").strip() + effective_work_place = server_device_name if server_device_name else (local_work_place or "notconfig") + p.set("device", "work_place", effective_work_place) + # Same guard for hostname: prefer server value, fall back to OS hostname, + # then whatever was in local config. Never blank it. + server_hostname = new_cfg.get("hostname", "").strip() + if not server_hostname: + try: + server_hostname = socket.gethostname() + except Exception: + server_hostname = APP_CONFIG.get("device_hostname", "").strip() + p.set("device", "hostname", server_hostname) + p.set("device", "ip", new_cfg.get("device_ip", "")) + p.set("device", "location", new_cfg.get("location") or new_cfg.get("device_location", "")) + p.set("device", "info_reviewed_at", new_cfg.get("info_reviewed_at") or "1970-01-01T00:00:00") + p.set("device", "card_presence", new_cfg.get("card_presence", "enable")) + + p.add_section("meta") + sync_ts = new_cfg.get("config_updated_at") or datetime.now().isoformat() + p.set("meta", "last_synced", sync_ts) + + os.makedirs("./data", exist_ok=True) + with open("./data/config.txt", "w") as f: + f.write("# WMT Application Configuration\n") + f.write(f"# Synced from server: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") + p.write(f) + print(f"\u2713 config.txt written from server (server ts: {sync_ts})") + + +def sync_config_with_server(): + """ + Called once at startup. + - Derives base URL from server_log_url in APP_CONFIG. + - If server config is newer than last_synced → pull and overwrite config.txt, reload APP_CONFIG. + - Otherwise → send a device-info update request to the server. + Returns True if config was pulled from server. + """ + global APP_CONFIG + try: + import requests as _req + import urllib.parse as _up + + parsed = _up.urlparse(APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs")) + server_base = f"{parsed.scheme}://{parsed.netloc}" + mac = _get_mac_address() + + last_synced_str = APP_CONFIG.get("last_synced", "1970-01-01T00:00:00") + try: + last_synced = datetime.fromisoformat(last_synced_str) + except Exception: + last_synced = datetime(1970, 1, 1) + + local_info_reviewed_str = APP_CONFIG.get("device_info_reviewed_at", "1970-01-01T00:00:00") + try: + local_info_reviewed = datetime.fromisoformat(local_info_reviewed_str) + except Exception: + local_info_reviewed = datetime(1970, 1, 1) + + print(f"Checking server config (MAC={mac}, last_synced={last_synced_str}, info_reviewed_at={local_info_reviewed_str}) ...") + + # --- Step 1: get server timestamp --- + ts_resp = _req.get( + f"{server_base}/api/wmt/config/timestamp", + params={"mac": mac}, timeout=5 + ) + ts_resp.raise_for_status() + ts_data = ts_resp.json() + + server_ts_str = ts_data.get("latest_updated_at", "1970-01-01T00:00:00") + try: + server_ts = datetime.fromisoformat(server_ts_str) + except Exception: + server_ts = datetime(1970, 1, 1) + + server_info_reviewed_str = ts_data.get("device_info_reviewed_at") or "1970-01-01T00:00:00" + try: + server_info_reviewed = datetime.fromisoformat(server_info_reviewed_str) + except Exception: + server_info_reviewed = datetime(1970, 1, 1) + + # Pull if global settings are newer OR if admin has reviewed device info more recently + needs_pull = server_ts > last_synced or server_info_reviewed > local_info_reviewed + + if needs_pull: + # --- Step 2a: pull config --- + print(f"Server config is newer ({server_ts_str}), pulling ...") + cfg_resp = _req.get(f"{server_base}/api/wmt/config/{mac}", timeout=5) + cfg_resp.raise_for_status() + _write_config_from_server(cfg_resp.json()) + APP_CONFIG = load_config() + print("\u2705 Config synced from server.") + return True + else: + # --- Step 2b: push device info --- + print("Local config is current, sending device info to server ...") + try: + _hostname = socket.gethostname() + _ip = socket.gethostbyname(_hostname) + except Exception: + _hostname = APP_CONFIG.get("device_hostname", "") + _ip = APP_CONFIG.get("device_ip", "") + + _req.post( + f"{server_base}/api/wmt/config/update_request", + json={ + "mac_address": mac, + "device_name": APP_CONFIG.get("work_place", ""), + "hostname": _hostname, + "device_ip": _ip, + "client_config_mtime": last_synced_str, + "client_info_reviewed_at": local_info_reviewed_str, + "card_presence": APP_CONFIG.get("card_presence", "enable"), + }, + timeout=5, + ) + print("\u2705 Device info update request sent.") + return False + + except Exception as e: + print(f"Config sync skipped (server unreachable or error): {e}") + return False + + +# Load global application configuration +APP_CONFIG = load_config() + +# Attempt to sync with server at startup (non-blocking – failures are logged and ignored) +sync_config_with_server() + +# --------------------------------------------------------------------------- +# Card presence flag – controls whether RFID functions are started +# --------------------------------------------------------------------------- +CARD_PRESENCE_ENABLED = APP_CONFIG.get("card_presence", "enable").strip().lower() == "enable" +print(f"Card presence: {'ENABLED' if CARD_PRESENCE_ENABLED else 'DISABLED'}") + +# Early configuration mode detection (before heavy initialization) +def early_launch_configuration_mode(): + """ + Early launch of configuration mode with chromium displaying Screen.html + """ + try: + print("🔧 Configuration mode detected - launching Screen.html in Chromium") + + # Path to Screen.html relative to the WMT working directory + screen_html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Files", "Screen.html") + screen_html_relative = "./Files/Screen.html" + if os.path.exists(screen_html_relative): + screen_html_path = os.path.abspath(screen_html_relative) + + if not os.path.exists(screen_html_path): + print(f"❌ Screen.html not found at: {screen_html_path}") + return False + + print(f"📄 Loading Screen.html from: {screen_html_path}") + + # Terminate any existing Chromium processes + chromium_process_name = "chromium" + # Local log only in configuration mode + logging.info("Refreshing Chromium process (configuration mode)") + + try: + subprocess.run(["pkill", "-f", chromium_process_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + time.sleep(5) # Wait for processes to terminate + # Launch Chromium with Screen.html + url = f"file://{screen_html_path}" + subprocess.Popen( + ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", + url], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True + ) + logging.info("Chromium process restarted successfully (configuration mode)") + except Exception as e: + logging.error(f"Failed to refresh Chromium process (configuration mode): {e}") + return False + + print("✅ Configuration mode launched successfully") + print("ℹ️ Network connectivity checks disabled") + print("ℹ️ Server status messages disabled") + print("🔧 Device running in configuration mode") + print("🔧 To exit configuration mode, change work_place in config.txt") + return True + + except FileNotFoundError: + print("❌ Chromium browser not found. Please install chromium-browser:") + print(" sudo apt update && sudo apt install chromium-browser") + return False + except Exception as e: + print(f"❌ Error launching configuration mode: {e}") + return False + +# EARLY CONFIGURATION MODE DETECTION +print("=" * 60) +print("CONFIGURATION MODE DETECTION") +print("=" * 60) + + +def _try_autoconfigure_from_prezenta(): + """ + If work_place is still 'notconfig', look for the idmasa.txt left behind by + the migration playbook in Prezenta_Old_Data and use it to self-configure. + Updates config.txt and reloads APP_CONFIG. + Returns True if a valid work_place was found and written. + """ + global APP_CONFIG + idmasa_candidates = [ + "/home/pi/Desktop/Prezenta_Old_Data/data/idmasa.txt", + "/home/pi/Desktop/Prezenta_Old_Data/Files/idmasa.txt", + "/home/pi/Desktop/Prezenta/data/idmasa.txt", + "/home/pi/Desktop/Prezenta/Files/idmasa.txt", + ] + for path in idmasa_candidates: + if os.path.exists(path): + try: + with open(path, "r") as f: + value = f.read().strip() + if value and value.lower() != "notconfig": + print(f"✅ Auto-configure: found work_place='{value}' in {path}") + # Write into config.txt [device] section + import configparser as _cp + p = _cp.ConfigParser() + p.read("./data/config.txt") + if not p.has_section("device"): + p.add_section("device") + p.set("device", "work_place", value) + # Also capture real hostname/IP + try: + _hn = socket.gethostname() + _ip = socket.gethostbyname(_hn) + except Exception: + _hn = APP_CONFIG.get("device_hostname", "") + _ip = APP_CONFIG.get("device_ip", "") + p.set("device", "hostname", _hn) + p.set("device", "ip", _ip) + os.makedirs("./data", exist_ok=True) + with open("./data/config.txt", "w") as f: + f.write("# WMT Application Configuration\n") + f.write(f"# Auto-configured from {path}\n\n") + p.write(f) + APP_CONFIG = load_config() + print(f"✅ config.txt updated: work_place={value}, hostname={_hn}, ip={_ip}") + return True + except Exception as e: + print(f"Warning: could not read {path}: {e}") + return False + + +def _send_first_registration_log(): + """ + Send device info to the server immediately after auto-configuration so the + server record is created before the periodic sync runs. + """ + try: + import requests as _req + import urllib.parse as _up + parsed = _up.urlparse(APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs")) + server_base = f"{parsed.scheme}://{parsed.netloc}" + mac = _get_mac_address() + try: + _hn = socket.gethostname() + _ip = socket.gethostbyname(_hn) + except Exception: + _hn = APP_CONFIG.get("device_hostname", "") + _ip = APP_CONFIG.get("device_ip", "") + _req.post( + f"{server_base}/api/wmt/config/update_request", + json={ + "mac_address": mac, + "device_name": APP_CONFIG.get("work_place", ""), + "hostname": _hn, + "device_ip": _ip, + "card_presence": APP_CONFIG.get("card_presence", "enable"), + "client_config_mtime": APP_CONFIG.get("last_synced", "1970-01-01T00:00:00"), + }, + timeout=5, + ) + print(f"✅ Registration sent to server (work_place={APP_CONFIG.get('work_place')}, hostname={_hn})") + except Exception as e: + print(f"Warning: could not send first registration to server: {e}") + + +try: + name = APP_CONFIG.get("work_place", "noconfig") + print(f"✓ Device name loaded: {name}") + + # Check if device is in configuration mode + if name.lower() == "notconfig": + print("🔧 Device configured for setup mode (notconfig)") + + # --- Try to self-configure from Prezenta_Old_Data before entering config mode --- + if _try_autoconfigure_from_prezenta(): + name = APP_CONFIG.get("work_place", "notconfig") + print(f"✅ Auto-configuration successful — work_place='{name}', skipping config mode") + _send_first_registration_log() + CONFIGURATION_MODE = False + else: + # No idmasa.txt found — fall through to manual configuration mode + if early_launch_configuration_mode(): + print("🚀 Configuration mode active - application will run in setup mode") + print("⚠️ Network connectivity checks are DISABLED") + print("⚠️ Server status messages are DISABLED") + print("🔧 Change config.txt [device] name to exit configuration mode") + CONFIGURATION_MODE = True + print("🔧 Configuration mode activated - continuing with RFID reader initialization") + print("🔧 Network and server monitoring will remain disabled") + else: + print("❌ Failed to launch configuration mode, continuing with normal operation") + CONFIGURATION_MODE = False + else: + print("✅ Device in normal operation mode") + CONFIGURATION_MODE = False + +except Exception as e: + print(f"Error in configuration mode detection: {e}") + name = "noconfig" + CONFIGURATION_MODE = False + +print("=" * 60) +print("CONTINUING WITH NORMAL INITIALIZATION" if not CONFIGURATION_MODE else "CONFIGURATION MODE ACTIVE") +print("=" * 60) + +# --------------------------------------------------------------------------- +# Auto-printer setup +# --------------------------------------------------------------------------- + +def _ensure_printer(device_name): + """ + Check whether a CUPS printer named *device_name* is installed. + If not found, install it automatically using lpadmin. + + Only called when work_place is set to a real name (not 'notconfig'). + """ + print(f"🖨 Checking printer for work_place='{device_name}' ...") + try: + import cups + conn = cups.Connection() + printers = conn.getPrinters() + if device_name in printers: + print(f"✓ Printer '{device_name}' already installed — no action needed.") + return + # Printer not found — install it + print(f"🖨 Printer '{device_name}' not found. Starting installation ...") + cmd = [ + "sudo", "/usr/sbin/lpadmin", + "-p", device_name, + "-E", + "-v", "usb://CITIZEN/CT-S310II?serial=00000000", + "-m", "CTS310II.ppd", + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + print(f"✓ Printer '{device_name}' installed successfully.") + else: + print(f"✗ lpadmin returned code {result.returncode}: {result.stderr.strip()}") + except ImportError: + print("⚠ python3-cups not available — printer check skipped. " + "Install with: sudo apt install python3-cups") + except Exception as e: + print(f"⚠ Printer setup skipped: {e}") + + +# Run auto-printer setup only when device has a real work_place +_auto_printer_name = APP_CONFIG.get("work_place", "notconfig") +if not CONFIGURATION_MODE and _auto_printer_name.lower() != "notconfig": + _ensure_printer(_auto_printer_name) +else: + if _auto_printer_name.lower() == "notconfig": + print("🖨 Printer setup skipped (work_place=notconfig).") + + +def check_system_requirements(): + """ + Check and set up system requirements for the application + """ + print("Checking system requirements...") + + # 1. Check and install required system packages + system_packages = { + 'sshpass': 'sshpass_1.09-1_armhf.deb' # Required for auto-update functionality + } + + for package, deb_file in system_packages.items(): + try: + result = subprocess.run(['which', package], capture_output=True, text=True) + if result.returncode == 0: + print(f"✓ {package} is installed") + else: + print(f"Installing {package}...") + + # Try online installation first + try: + install_result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True, text=True, timeout=120) + install_result = subprocess.run(['sudo', 'apt', 'install', '-y', package], + capture_output=True, text=True, timeout=300) + if install_result.returncode == 0: + print(f"✓ {package} installed successfully (online)") + continue + else: + print(f"Online installation failed, trying offline...") + except Exception as online_error: + print(f"Online installation failed: {online_error}, trying offline...") + + # Try offline installation from local .deb file + deb_path = f"./Files/system_packages/{deb_file}" + if os.path.exists(deb_path): + try: + print(f"Installing {package} from local package: {deb_path}") + offline_result = subprocess.run(['sudo', 'dpkg', '-i', deb_path], + capture_output=True, text=True, timeout=120) + if offline_result.returncode == 0: + print(f"✓ {package} installed successfully (offline)") + else: + print(f"✗ Offline installation failed: {offline_result.stderr}") + # Try to fix dependencies + print("Attempting to fix dependencies...") + subprocess.run(['sudo', 'apt', '--fix-broken', 'install', '-y'], + capture_output=True, text=True, timeout=300) + except Exception as offline_error: + print(f"✗ Offline installation error: {offline_error}") + else: + print(f"✗ Local package not found: {deb_path}") + print(f" To add offline support, download with: apt download {package}") + + except Exception as e: + print(f"Warning: Could not check/install {package}: {e}") + + # 2. Check and create required directories + required_dirs = ['./data', './Files', './Files/reposytory', './Files/system_packages'] + for dir_path in required_dirs: + try: + os.makedirs(dir_path, exist_ok=True) + print(f"✓ Directory ensured: {dir_path}") + except Exception as e: + print(f"✗ Failed to create directory {dir_path}: {e}") + return False + + # 2. Check required files and create defaults if missing + required_files = { + './data/log.txt': '', + './data/tag.txt': '', + './data/device_info.txt': 'unknown-device\n127.0.0.1\n' + } + + for file_path, default_content in required_files.items(): + try: + if not os.path.exists(file_path): + with open(file_path, 'w') as f: + f.write(default_content) + print(f"✓ Created default file: {file_path}") + else: + print(f"✓ File exists: {file_path}") + except Exception as e: + print(f"✗ Failed to create file {file_path}: {e}") + + # 3. Check file permissions + try: + for file_path in required_files.keys(): + if os.path.exists(file_path): + # Ensure read/write permissions for the application + os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + print("✓ File permissions set correctly") + except Exception as e: + print(f"Warning: Could not set file permissions: {e}") + + return True + +def check_port_capabilities(): + """ + Check if the application can bind to port 80 and set up capabilities if needed + """ + print("Checking port 80 capabilities...") + + try: + # Check if we're running as root + if os.geteuid() == 0: + print("✓ Running as root - port 80 access available") + return True + + # Check if capabilities are set + python_path = sys.executable + result = subprocess.run(['getcap', python_path], capture_output=True, text=True) + + if 'cap_net_bind_service=ep' in result.stdout: + print("✓ Port binding capabilities already set") + return True + + # Try to set capabilities + print("Setting up port 80 binding capabilities...") + setup_script = './setup_port_capability.sh' + + if os.path.exists(setup_script): + result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) + if result.returncode == 0: + print("✓ Port capabilities set successfully") + return True + else: + print(f"✗ Failed to set capabilities: {result.stderr}") + else: + # Create the setup script if it doesn't exist + script_content = f'''#!/bin/bash +# Set port binding capability for Python to allow port 80 access +echo "Setting port binding capability for Python..." +sudo setcap cap_net_bind_service=ep {python_path} +echo "Capability set successfully" +''' + try: + with open(setup_script, 'w') as f: + f.write(script_content) + os.chmod(setup_script, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH) + + result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True) + if result.returncode == 0: + print("✓ Port capabilities set successfully") + return True + else: + print(f"✗ Failed to set capabilities: {result.stderr}") + except Exception as e: + print(f"✗ Failed to create setup script: {e}") + + except Exception as e: + print(f"Warning: Could not check port capabilities: {e}") + + print("Warning: Port 80 may not be accessible. App will try to run on default port.") + return False + +def check_hardware_interfaces(): + """ + Check hardware interfaces (UART/Serial) required for RFID reader + """ + print("Checking hardware interfaces...") + + # Check for serial devices + serial_devices = ['/dev/ttyS0', '/dev/ttyAMA0', '/dev/ttyUSB0'] + available_devices = [] + + for device in serial_devices: + if os.path.exists(device): + try: + # Check if we can access the device + with open(device, 'r'): + pass + available_devices.append(device) + print(f"✓ Serial device available: {device}") + except PermissionError: + print(f"✗ Permission denied for {device}. Adding user to dialout group...") + try: + # Add current user to dialout group for serial access + username = pwd.getpwuid(os.getuid()).pw_name + subprocess.run(['sudo', 'usermod', '-a', '-G', 'dialout', username], + capture_output=True, text=True) + print(f"✓ User {username} added to dialout group (reboot may be required)") + available_devices.append(device) + except Exception as e: + print(f"✗ Failed to add user to dialout group: {e}") + except Exception as e: + print(f"Warning: Could not test {device}: {e}") + + if not available_devices: + print("✗ No serial devices found. RFID reader may not work.") + # Enable UART if we're on Raspberry Pi + try: + config_file = '/boot/config.txt' + if os.path.exists(config_file): + print("Attempting to enable UART in Raspberry Pi config...") + result = subprocess.run(['sudo', 'raspi-config', 'nonint', 'do_serial', '0'], + capture_output=True, text=True) + if result.returncode == 0: + print("✓ UART enabled in config (reboot required)") + else: + print("Warning: Could not enable UART automatically") + except Exception as e: + print(f"Warning: Could not configure UART: {e}") + return False + + return True + +def check_network_connectivity(): + """ + Check network connectivity and DNS resolution + """ + print("Checking network connectivity...") + + try: + # Test basic connectivity + result = subprocess.run(['ping', '-c', '1', '8.8.8.8'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print("✓ Internet connectivity available") + + # Test DNS resolution + try: + import socket + socket.gethostbyname('google.com') + print("✓ DNS resolution working") + return True + except socket.gaierror: + print("✗ DNS resolution failed") + return False + else: + print("✗ No internet connectivity") + return False + + except subprocess.TimeoutExpired: + print("✗ Network timeout") + return False + except Exception as e: + print(f"Warning: Could not test network: {e}") + return False + +def initialize_gpio_permissions(): + """ + Set up GPIO permissions for LED control + """ + print("Setting up GPIO permissions...") + + try: + # Add user to gpio group if it exists + username = pwd.getpwuid(os.getuid()).pw_name + + # Check if gpio group exists + try: + grp.getgrnam('gpio') + subprocess.run(['sudo', 'usermod', '-a', '-G', 'gpio', username], + capture_output=True, text=True) + print(f"✓ User {username} added to gpio group") + except KeyError: + print("Warning: gpio group not found - GPIO access may be limited") + + # Set up GPIO access via /dev/gpiomem if available + gpio_devices = ['/dev/gpiomem', '/dev/mem'] + for device in gpio_devices: + if os.path.exists(device): + print(f"✓ GPIO device available: {device}") + break + else: + print("Warning: No GPIO devices found") + + except Exception as e: + print(f"Warning: Could not set up GPIO permissions: {e}") + +def perform_system_initialization(): + """ + Perform complete system initialization for first run + """ + print("=" * 60) + print("SYSTEM INITIALIZATION - Preparing for first run") + print("=" * 60) + + initialization_steps = [ + ("System Requirements", check_system_requirements), + ("Port Capabilities", check_port_capabilities), + ("Hardware Interfaces", check_hardware_interfaces), + ("GPIO Permissions", initialize_gpio_permissions), + ("Network Connectivity", check_network_connectivity) + ] + + success_count = 0 + total_steps = len(initialization_steps) + + for step_name, step_function in initialization_steps: + print(f"\n--- {step_name} ---") + try: + if step_function(): + success_count += 1 + print(f"✓ {step_name} completed successfully") + else: + print(f"⚠ {step_name} completed with warnings") + except Exception as e: + print(f"✗ {step_name} failed: {e}") + + print("\n" + "=" * 60) + print(f"INITIALIZATION COMPLETE: {success_count}/{total_steps} steps successful") + print("=" * 60) + + if success_count < total_steps: + print("Warning: Some initialization steps failed. Application may have limited functionality.") + print("Check the messages above for details.") + + return success_count >= (total_steps - 1) # Allow one failure + +#configurare variabile +def get_device_info(): + """ + Get hostname and device IP with file-based fallback to avoid socket errors + """ + config_file = "./data/device_info.txt" + hostname = None + device_ip = None + + # Try to get current hostname and IP + try: + hostname = socket.gethostname() + device_ip = socket.gethostbyname(hostname) + print(f"Successfully resolved - Hostname: {hostname}, IP: {device_ip}") + + # Save the working values to file for future fallback + try: + os.makedirs("./data", exist_ok=True) # Create data directory if it doesn't exist + with open(config_file, "w") as f: + f.write(f"{hostname}\n{device_ip}\n") + print(f"Saved device info to {config_file}") + except Exception as e: + print(f"Warning: Could not save device info to file: {e}") + + return hostname, device_ip + + except socket.gaierror as e: + print(f"Socket error occurred: {e}") + print("Attempting to load device info from file...") + + # Try to load from file + try: + with open(config_file, "r") as f: + lines = f.read().strip().split('\n') + if len(lines) >= 2: + hostname = lines[0].strip() + device_ip = lines[1].strip() + print(f"Loaded from file - Hostname: {hostname}, IP: {device_ip}") + return hostname, device_ip + else: + print("File exists but doesn't contain valid data") + except FileNotFoundError: + print(f"No fallback file found at {config_file}") + except Exception as e: + print(f"Error reading fallback file: {e}") + + except Exception as e: + print(f"Unexpected error getting device info: {e}") + + # Try to load from file as fallback + try: + with open(config_file, "r") as f: + lines = f.read().strip().split('\n') + if len(lines) >= 2: + hostname = lines[0].strip() + device_ip = lines[1].strip() + print(f"Loaded from file after error - Hostname: {hostname}, IP: {device_ip}") + return hostname, device_ip + except Exception as file_error: + print(f"Could not load from file: {file_error}") + + # Final fallback: use values from APP_CONFIG (config.txt [device] section) + try: + cfg_hostname = APP_CONFIG.get("device_hostname", "") + cfg_ip = APP_CONFIG.get("device_ip", "") + if cfg_hostname and cfg_hostname != "unknown-device": + hostname = cfg_hostname + device_ip = cfg_ip or "127.0.0.1" + print(f"Loaded from config.txt - Hostname: {hostname}, IP: {device_ip}") + return hostname, device_ip + except Exception: + pass + + # Absolute last resort defaults + print("All methods failed - Using default values") + hostname = hostname or "unknown-device" + device_ip = "127.0.0.1" + + # Try to save these default values for next time + try: + os.makedirs("./data", exist_ok=True) + with open(config_file, "w") as f: + f.write(f"{hostname}\n{device_ip}\n") + print(f"Saved fallback values to {config_file}") + except Exception as e: + print(f"Could not save fallback values: {e}") + + return hostname, device_ip + +# Perform system initialization (first run setup) +if not perform_system_initialization(): + print("Warning: System initialization completed with errors.") + print("The application will continue but may have limited functionality.") + +# Get device information with error handling +hostname, device_ip = get_device_info() +print(f"Final result - Hostname: {hostname}, Device IP: {device_ip}") +# Configure logging +logging.basicConfig(filename='./data/log.txt', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# function to delete old logs +def delete_old_logs(): + log_dir = './data/' + log_file = 'log.txt' + log_path = os.path.join(log_dir, log_file) + + if os.path.exists(log_path): + file_mod_time = datetime.fromtimestamp(os.path.getmtime(log_path)) + if datetime.now() - file_mod_time > timedelta(days=10): + os.remove(log_path) + log_info_with_server(f"Deleted old log file: {log_file}") + else: + log_info_with_server(f"Log file is not older than 10 days: {log_file}") + else: + log_info_with_server(f"Log file does not exist: {log_file}") + +# Function to read the work place name from config +def read_name_from_file(): + try: + n_masa = APP_CONFIG.get("work_place", "") + if n_masa: + return n_masa + except Exception: + pass + return "unknown" + +def _get_os_version() -> str: + """Read OS pretty-name from /etc/os-release (Raspberry Pi OS, etc.).""" + try: + with open("/etc/os-release") as f: + for line in f: + if line.startswith("PRETTY_NAME="): + return line.split("=", 1)[1].strip().strip('"') + except Exception: + pass + return "" + +# Function to send logs to a remote server for the Server_monitorizare APP +def send_log_to_server(log_message, n_masa, hostname, device_ip): + try: + log_data = { + "hostname": str(hostname), + "device_ip": str(device_ip), + "nume_masa": str(n_masa), + "log_message": str(log_message), + # Device metadata – keeps the server record up to date automatically + "device_type": "Raspberry Pi", + "os_version": _get_os_version(), + "location": APP_CONFIG.get("device_location", ""), + "mac_address": _get_mac_address(), + "card_presence": APP_CONFIG.get("card_presence", "enable"), + } + server_url = APP_CONFIG.get("server_log_url", "http://rpi-ansible:80/logs") + print(log_data) # Debugging: Print log_data to verify its contents + response = requests.post(server_url, json=log_data, timeout=5) + response.raise_for_status() + logging.info("Log successfully sent to server: %s", log_message) + except requests.exceptions.RequestException as e: + logging.error("Failed to send log to server: %s", e) +# Wrapper for logging.info to also send logs to the server Monitorizare APP +def log_info_with_server(message): + n_masa = read_name_from_file() # Read work place name from config + formatted_message = f"{message} (n_masa: {n_masa})" # Format the message + logging.info(formatted_message) # Log the formatted message + + # Only send to server if not in configuration mode + try: + if not CONFIGURATION_MODE: + send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server + else: + logging.info("Configuration mode: Server logging disabled") + except NameError: + # CONFIGURATION_MODE not defined yet (during initialization) + send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server + +# Function to execute system commands with proper security +def execute_system_command(command): + """ + Execute system commands with proper logging and security checks + """ + # Define allowed commands for security + allowed_commands = [ + "sudo apt update", + "sudo apt upgrade -y", + "sudo apt update && sudo apt upgrade -y", # Combined update and upgrade + "sudo apt autoremove -y", + "sudo apt autoclean", + "sudo reboot", + "sudo shutdown -h now", + "df -h", + "free -m", + "uptime", + "systemctl status", + "sudo systemctl restart networking", + "sudo systemctl restart ssh" + ] + + try: + # Check if command is allowed + if command not in allowed_commands: + log_info_with_server(f"Command '{command}' is not allowed for security reasons") + return {"status": "error", "message": f"Command '{command}' is not allowed", "output": ""} + + log_info_with_server(f"Executing command: {command}") + + # Execute the command + result = subprocess.run( + command.split(), + capture_output=True, + text=True, + timeout=300 # 5 minute timeout + ) + + output = result.stdout + result.stderr + + if result.returncode == 0: + log_info_with_server(f"Command '{command}' executed successfully") + return {"status": "success", "message": "Command executed successfully", "output": output} + else: + log_info_with_server(f"Command '{command}' failed with return code {result.returncode}") + return {"status": "error", "message": f"Command failed with return code {result.returncode}", "output": output} + + except subprocess.TimeoutExpired: + log_info_with_server(f"Command '{command}' timed out") + return {"status": "error", "message": "Command timed out", "output": ""} + except Exception as e: + log_info_with_server(f"Error executing command '{command}': {str(e)}") + return {"status": "error", "message": f"Error: {str(e)}", "output": ""} + +# Flask app for receiving commands (only if Flask is available) +if FLASK_AVAILABLE: + command_app = Flask(__name__) + + @command_app.route('/execute_command', methods=['POST']) + def handle_command_execution(): + """ + Endpoint to receive and execute system commands + """ + try: + data = request.json + if not data or 'command' not in data: + return jsonify({"error": "Invalid request. 'command' field is required"}), 400 + + command = data.get('command') + + # Execute the command + result = execute_system_command(command) + + return jsonify(result), 200 if result['status'] == 'success' else 400 + + except Exception as e: + log_info_with_server(f"Error handling command execution request: {str(e)}") + return jsonify({"error": f"Server error: {str(e)}"}), 500 + + @command_app.route('/status', methods=['GET']) + def get_device_status(): + """ + Endpoint to get device status information + """ + try: + n_masa = read_name_from_file() + + # Get system information + uptime_result = subprocess.run(['uptime'], capture_output=True, text=True) + df_result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True) + free_result = subprocess.run(['free', '-m'], capture_output=True, text=True) + + status_info = { + "hostname": hostname, + "device_ip": device_ip, + "nume_masa": n_masa, + "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "uptime": uptime_result.stdout.strip() if uptime_result.returncode == 0 else "N/A", + "disk_usage": df_result.stdout.strip() if df_result.returncode == 0 else "N/A", + "memory_usage": free_result.stdout.strip() if free_result.returncode == 0 else "N/A" + } + + return jsonify(status_info), 200 + + except Exception as e: + log_info_with_server(f"Error getting device status: {str(e)}") + return jsonify({"error": f"Error getting status: {str(e)}"}), 500 + + @command_app.route('/auto_update', methods=['POST']) + def auto_update_app(): + """ + Trigger an immediate WMT client update check against the monitoring server. + Delegates to _check_and_apply_update(). + """ + result = _check_and_apply_update() + if result.get('updated'): + return jsonify({"status": "success", "message": result['message']}), 200 + elif result.get('error'): + return jsonify({"error": result['message']}), 500 + else: + return jsonify({"status": "no_update", "message": result['message']}), 200 + + @command_app.route('/update_config', methods=['POST']) + def update_config_endpoint(): + """ + Update configuration from Server_Monitorizare_v2. + Accepts a JSON body with sections matching config.txt structure. + Example body: {"chrome": {"chrome_url": "http://..."}} + """ + global APP_CONFIG + + ALLOWED_SECTIONS = { + "chrome": ["chrome_url", "chrome_local_url", "chrome_insecure_origin"], + "card_api": ["base_url"], + "server": ["log_url", "update_host", "update_user", "internet_check_host"], + "device": ["name", "hostname", "ip"], + } + + try: + data = request.json + if not data: + return jsonify({"error": "JSON body required"}), 400 + + config_path = "./data/config.txt" + parser = configparser.ConfigParser() + if os.path.exists(config_path): + parser.read(config_path) + + updated_keys = [] + for section, allowed_keys in ALLOWED_SECTIONS.items(): + if section in data and isinstance(data[section], dict): + if not parser.has_section(section): + parser.add_section(section) + for key in allowed_keys: + if key in data[section]: + parser.set(section, key, str(data[section][key])) + updated_keys.append(f"{section}.{key}") + + os.makedirs("./data", exist_ok=True) + with open(config_path, "w") as f: + f.write("# WMT Application Configuration\n") + f.write(f"# Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") + parser.write(f) + + APP_CONFIG = load_config() + log_info_with_server(f"Configuration updated via API: {updated_keys}") + return jsonify({"status": "success", "updated_keys": updated_keys}), 200 + + except Exception as e: + log_info_with_server(f"Error updating config: {str(e)}") + return jsonify({"error": f"Failed to update configuration: {str(e)}"}), 500 + + @command_app.route('/reload_config', methods=['POST']) + def reload_config_endpoint(): + """ + Reload configuration from data/config.txt into memory without restarting. + """ + global APP_CONFIG + try: + APP_CONFIG = load_config() + log_info_with_server("Configuration reloaded via API") + return jsonify({ + "status": "success", + "message": "Configuration reloaded", + "chrome_url": APP_CONFIG.get("chrome_url"), + "card_post_base_url": APP_CONFIG.get("card_post_base_url"), + "server_log_url": APP_CONFIG.get("server_log_url"), + "device_name": APP_CONFIG.get("work_place"), + }), 200 + except Exception as e: + return jsonify({"error": f"Failed to reload config: {str(e)}"}), 500 + + def start_command_server(): + """ + Start the Flask server with enhanced port handling and fallback + """ + # Try different ports in order of preference + preferred_ports = [ + int(os.environ.get('FLASK_PORT', 80)), # Use environment variable or default to 80 + 80, # Standard HTTP port + 5000, # Flask default + 8080, # Alternative HTTP port + 3000 # Development port + ] + + for port in preferred_ports: + try: + print(f"Attempting to start command server on port {port}...") + + # Test if port is available + import socket + test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + test_socket.bind(('0.0.0.0', port)) + test_socket.close() + + # Port is available, start Flask server + print(f"Port {port} is available. Starting command server...") + command_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False) + return # Success, exit function + + except PermissionError: + print(f"✗ Permission denied for port {port}") + if port == 80: + print(" Hint: Port 80 requires root privileges or capabilities") + print(" Try running: sudo setcap cap_net_bind_service=ep $(which python3)") + continue + except OSError as e: + if "Address already in use" in str(e): + print(f"✗ Port {port} is already in use") + else: + print(f"✗ Port {port} error: {e}") + continue + except Exception as e: + print(f"✗ Failed to start on port {port}: {e}") + continue + + # If we get here, all ports failed + log_info_with_server("Error: Could not start command server on any port") + print("✗ Could not start command server on any available port") + + # Start command server in a separate process with enhanced error handling + try: + print("Initializing command server...") + command_server_process = Process(target=start_command_server) + command_server_process.daemon = True # Ensure it dies with main process + command_server_process.start() + + # Give the server a moment to start and check if it's running + import time + time.sleep(2) + + if command_server_process.is_alive(): + port = int(os.environ.get('FLASK_PORT', 80)) + print(f"✓ Command server started successfully on port {port}") + else: + print("Warning: Command server process stopped unexpectedly") + + except Exception as e: + print(f"Warning: Could not start command server: {e}") + log_info_with_server(f"Command server startup error: {str(e)}") +else: + print("Warning: Flask not available - Command server disabled") +# Call the function to delete old logs +delete_old_logs() +def config(): + import config +# function for posting data to the harting server +def post_backup_data(): + + try: + with open("./data/tag.txt", "r") as file: + lines = file.readlines() + remaining_lines = lines[:] + + for line in lines: + line = line.strip() + if line: + + try: + response = requests.post(line, verify=False, timeout=3) +# + response.raise_for_status() # Raise an error for bad status codes + log_info_with_server(f"Data posted successfully:") + remaining_lines.remove(line + "\n") + except requests.exceptions.Timeout: + log_info_with_server("Request timed out.") + break + except requests.exceptions.RequestException as e: + log_info_with_server(f"An error occurred: ") + break + + with open("./data/tag.txt", "w") as file: + file.writelines(remaining_lines) + #log_info_with_server("Backup data updated.") + + except FileNotFoundError: + log_info_with_server("No backup file found.") + + +# Function to check internet connection +def check_internet_connection(): + hostname = APP_CONFIG.get("internet_check_host", "filesibiusb05.sibiusb.harting.intra") + cmd_block_wifi = 'sudo rfkill block wifi' + cmd_unblock_wifi = 'sudo rfkill unblock wifi' + log_info_with_server('Internet connection check loaded') + delete_old_logs() + chromium_process_name = "chromium" + + while True: + try: + # Use subprocess to execute the ping command + response = subprocess.run( + ["ping", "-c", "1", hostname], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + if response.returncode == 0: + log_info_with_server("Internet is up! Waiting 45 minutes.") + post_backup_data() + time.sleep(2700) # 45 minutes + else: + log_info_with_server("Internet is down. Rebooting WiFi.") + os.system(cmd_block_wifi) + time.sleep(1200) # 20 minutes + os.system(cmd_unblock_wifi) + + # Refresh Chromium process + log_info_with_server("Refreshing Chromium process.") + try: + # Find and terminate Chromium processes + subprocess.run(["pkill", "-f", chromium_process_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + time.sleep(5) # Wait for processes to terminate + + # Relaunch Chromium + url = APP_CONFIG.get("chrome_url", "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production") + chrome_insecure = APP_CONFIG.get("chrome_insecure_origin", "http://filesibiusb05.sibiusb.harting.intra") + subprocess.Popen( + ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", + f"--unsafely-treat-insecure-origin-as-secure={chrome_insecure}", url], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True + ) + log_info_with_server("Chromium process restarted successfully.") + except Exception as e: + log_info_with_server(f"Failed to refresh Chromium process: {e}") + except Exception as e: + log_info_with_server(f"An error occurred during internet check: {e}") + time.sleep(60) # Retry after 1 minute in case of an error + +# Start the internet connection check in a separate process (only if not in configuration mode) +if not CONFIGURATION_MODE: + internet_check_process = Process(target=check_internet_connection) + internet_check_process.start() + print("✅ Internet connectivity monitoring started") +else: + print("🔧 Configuration mode: Internet connectivity monitoring DISABLED") + +# --------------------------------------------------------------------------- +# HTTP-based client auto-update helpers +# --------------------------------------------------------------------------- + +def _get_local_version(): + """Return local app version as float by reading the first line of this file.""" + try: + with open(os.path.abspath(__file__), 'r') as f: + first_line = f.readline() + m = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE) + if m: + return float(m.group(1)) + except Exception: + pass + return None + + +def _check_and_apply_update(): + """ + Query the monitoring server for the latest WMT release. + If a newer version is available, download the zip, back up app.py, + extract into the WMT directory, and schedule a systemd service restart. + Returns a dict: {updated, message, error}. + """ + server_host = APP_CONFIG.get("server_host", "") + server_port = APP_CONFIG.get("server_port", "5000") + if not server_host: + return {"updated": False, "error": False, "message": "server_host not configured – skipping update check"} + + base_url = f"http://{server_host}:{server_port}" + local_version = _get_local_version() + + try: + resp = requests.get(f"{base_url}/api/wmt/client/version", timeout=10) + if resp.status_code != 200: + return {"updated": False, "error": True, "message": f"Version endpoint returned {resp.status_code}"} + meta = resp.json() + server_version = float(meta.get("version", 0)) + except Exception as e: + return {"updated": False, "error": True, "message": f"Could not reach version endpoint: {e}"} + + if local_version is not None and server_version <= local_version: + return {"updated": False, "error": False, "message": f"Already on latest version {local_version}"} + + log_info_with_server(f"WMT update available: local={local_version} server={server_version} – downloading …") + + wmt_dir = os.path.dirname(os.path.abspath(__file__)) + tmp_zip = "/tmp/wmt_update.zip" + app_py = os.path.join(wmt_dir, "app.py") + + try: + # Download the zip + dl = requests.get(f"{base_url}/api/wmt/client/download", timeout=60, stream=True) + if dl.status_code != 200: + return {"updated": False, "error": True, "message": f"Download endpoint returned {dl.status_code}"} + with open(tmp_zip, 'wb') as f: + for chunk in dl.iter_content(chunk_size=65536): + if chunk: + f.write(chunk) + + # Validate the zip + import zipfile + if not zipfile.is_zipfile(tmp_zip): + return {"updated": False, "error": True, "message": "Downloaded file is not a valid zip"} + + # Backup current app.py + bak = f"{app_py}.bak.{local_version or 'old'}" + try: + import shutil + shutil.copy2(app_py, bak) + except Exception as e: + log_info_with_server(f"Warning: could not back up app.py: {e}") + + # Extract into WMT directory – skip anything inside data/ to preserve device config + with zipfile.ZipFile(tmp_zip, 'r') as zf: + for member in zf.infolist(): + # Normalise path separators and skip the data folder + member_path = member.filename.replace('\\', '/') + if member_path.startswith('data/') or member_path == 'data': + continue + zf.extract(member, wmt_dir) + + log_info_with_server(f"WMT updated to version {server_version} – scheduling service restart") + + # Schedule restart via systemd (non-blocking) + subprocess.Popen( + ["bash", "-c", "sleep 3 && sudo systemctl restart wmt"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, start_new_session=True + ) + + return {"updated": True, "error": False, + "message": f"Updated from {local_version} to {server_version}. Restarting service …"} + + except Exception as e: + log_info_with_server(f"WMT auto-update failed: {e}") + return {"updated": False, "error": True, "message": str(e)} + finally: + try: + os.remove(tmp_zip) + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Periodic server config sync (background thread) +# --------------------------------------------------------------------------- + +def _periodic_config_sync(): + """ + Background thread: re-checks the server for config updates every 5 minutes. + When a new config is pulled, Chromium is restarted with the updated URLs. + """ + sync_interval = 300 # seconds (5 minutes) + while True: + time.sleep(sync_interval) + try: + config_changed = sync_config_with_server() + if config_changed and not CONFIGURATION_MODE: + print("🔄 Config changed – restarting Chromium with updated settings.") + try: + subprocess.run(["pkill", "-f", "chromium"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + time.sleep(5) + _url = APP_CONFIG.get("chrome_url", "") + _insecure = APP_CONFIG.get("chrome_insecure_origin", "") + subprocess.Popen( + ["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", + f"--unsafely-treat-insecure-origin-as-secure={_insecure}", _url], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, start_new_session=True + ) + print("✅ Chromium restarted with new config.") + except Exception as e: + print(f"Warning: Could not restart Chromium after config update: {e}") + except Exception as e: + print(f"Periodic config sync error: {e}") + + # Check for WMT client update every cycle (non-fatal) + try: + _check_and_apply_update() + except Exception as e: + print(f"Periodic update check error: {e}") + +if not CONFIGURATION_MODE: + _sync_thread = threading.Thread(target=_periodic_config_sync, daemon=True, name="config-sync") + _sync_thread.start() + print("✅ Periodic server config sync started (every 5 min)") + # Startup version check (non-blocking) + _update_thread = threading.Thread(target=_check_and_apply_update, daemon=True, name="startup-update-check") + _update_thread.start() +else: + print("🔧 Configuration mode: Periodic config sync DISABLED") + +# Launch Chromium with the specified URLs (only if not in configuration mode) +if not CONFIGURATION_MODE: + url = APP_CONFIG.get("chrome_url", "http://filesibiusb05.sibiusb.harting.intra/iweb_v2/index.php/traceability/production") + chrome_insecure = APP_CONFIG.get("chrome_insecure_origin", "http://filesibiusb05.sibiusb.harting.intra") + subprocess.Popen(["chromium", "--test-type", "--noerrors", "--kiosk", "--start-fullscreen", f"--unsafely-treat-insecure-origin-as-secure={chrome_insecure}", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, start_new_session=True) + print("✅ Main application browser launched") +else: + print("🔧 Configuration mode: Main application browser launch DISABLED") + +info = "0" +#function to post info +def post_info(info): + #log_info_with_server("Starting to post data...") + info1 = info.strip() # Remove any leading/trailing whitespace, including newlines + try: + response = requests.post(info1, verify=False, timeout=3) + response.raise_for_status() # Raise an error for bad status codes + #log_info_with_server("Data posted successfully") + except requests.exceptions.Timeout: + with open("./data/tag.txt", "a") as file: # Open in append mode + file.write(info) + log_info_with_server(f"Value was saved to tag.txt") + except requests.exceptions.RequestException as e: + with open("./data/tag.txt", "a") as file: # Open in append mode + file.write(info) + log_info_with_server("Value was saved to tag.txt") + +async def post_info_async(info): + try: + # Try to use aiohttp if available + if 'aiohttp' in globals(): + async with aiohttp.ClientSession() as session: + try: + async with session.post(info, ssl=False, timeout=3) as response: + response_text = await response.text() + log_info_with_server(f"Data posted successfully") + except asyncio.TimeoutError: + with open("./data/tag.txt", "a") as file: + file.write(info) + except Exception as e: + with open("./data/tag.txt", "a") as file: + file.write(info) + else: + # Fallback to synchronous requests if aiohttp not available + try: + response = requests.post(info.strip(), verify=False, timeout=3) + response.raise_for_status() + log_info_with_server(f"Data posted successfully (fallback)") + except requests.exceptions.Timeout: + with open("./data/tag.txt", "a") as file: + file.write(info) + except requests.exceptions.RequestException as e: + with open("./data/tag.txt", "a") as file: + file.write(info) + except Exception as e: + # Final fallback - save to file + with open("./data/tag.txt", "a") as file: + file.write(info) + +def post_info_thread(info): + try: + thread = threading.Thread(target=asyncio.run, args=(post_info_async(info),), daemon=True) + thread.start() + except Exception as e: + # Fallback to synchronous posting if async fails + print(f"Async posting failed, using sync fallback: {e}") + post_info(info) + +# Initialize LEDs – only when card reader is active +class DummyLED: + def __init__(self, pin): + self.pin = pin + def on(self): + pass + def off(self): + pass + +if CARD_PRESENCE_ENABLED: + try: + print("Initializing LED controls...") + led1 = OutputDevice(23) + led2 = OutputDevice(24) + print("✓ LED controls initialized successfully") + log_info_with_server("LED controls initialized") + except Exception as e: + print(f"Warning: Could not initialize LED controls: {e}") + print("Creating dummy LED objects - visual feedback will be disabled") + led1 = DummyLED(23) + led2 = DummyLED(24) + log_info_with_server("LED controls using dummy mode") +else: + print("🚫 Card presence disabled – LED controls skipped") + led1 = DummyLED(23) + led2 = DummyLED(24) + +# Initialize table name/ID +print("Initializing device configuration...") +name = APP_CONFIG.get("work_place", "noconfig") +logging.info("LED controls initialized") + +logging.info("Variabila Id Masa A fost initializata ") + +print(f"✓ Device name set from config: {name}") +if not CONFIGURATION_MODE: + log_info_with_server(f"Device name initialized: {name}") + +logging.info(name) +#clasa reader +class Reader(rdm6300.BaseReader): + global info + def card_inserted(self, card): + if card.value == 12886709: + + config() + return + + afisare = time.strftime("%Y-%m-%d&%H:%M:%S") + _base = APP_CONFIG.get("card_post_base_url", "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record") + date = f'{_base}/{name}/{card.value}/1/{afisare}\n' + info = date + if name == "noconfig": + led1.on() + time.sleep(5) + led1.off() + log_info_with_server(f"card inserted {card} but no") + else: + post_info_thread(info) + led1.on() + log_info_with_server(f"card inserted {card}") + + + def card_removed(self, card): + if card.value == 12886709: + log_info_with_server("Removing Config card") + return + + afisare = time.strftime("%Y-%m-%d&%H:%M:%S") + _base = APP_CONFIG.get("card_post_base_url", "https://dataswsibiusb01.sibiusb.harting.intra/RO_Quality_PRD/api/record") + date = f'{_base}/{name}/{card.value}/0/{afisare}\n' + info = date + if name == "noconfig": + led1.off() + log_info_with_server(f"card removed {card}") + else: + post_info_thread(info) + led1.off() + log_info_with_server(f"card removed {card}") + + + + +# Initialize RFID Reader with comprehensive error handling +def initialize_rfid_reader(): + """ + Initialize RFID reader with multiple device attempts and error handling + """ + print("Initializing RFID reader...") + + # List of possible serial devices in order of preference + serial_devices = ['/dev/ttyS0', '/dev/ttyAMA0', '/dev/ttyUSB0', '/dev/ttyACM0'] + + for device in serial_devices: + try: + print(f"Attempting to initialize RFID reader on {device}...") + r = Reader(device) + r.start() + print(f"✓ RFID reader successfully initialized on {device}") + log_info_with_server(f"RFID reader started on {device}") + return r + except FileNotFoundError: + print(f"✗ Device {device} not found") + continue + except PermissionError: + print(f"✗ Permission denied for {device}") + print(f" Hint: Try adding user to dialout group: sudo usermod -a -G dialout $USER") + continue + except Exception as e: + print(f"✗ Failed to initialize on {device}: {e}") + continue + + # If we get here, all devices failed + print("✗ Could not initialize RFID reader on any device") + print("Available solutions:") + print(" 1. Check hardware connections") + print(" 2. Enable UART: sudo raspi-config -> Interface Options -> Serial") + print(" 3. Add user to dialout group: sudo usermod -a -G dialout $USER") + print(" 4. Reboot the system after making changes") + + log_info_with_server("ERROR: RFID reader initialization failed") + return None + +# Start RFID reader only if card_presence is enabled +if CARD_PRESENCE_ENABLED: + try: + rfid_reader = initialize_rfid_reader() + if rfid_reader is None: + print("WARNING: Application starting without RFID functionality") + print("Card reading will not work until RFID reader is properly configured") + except Exception as e: + print(f"Critical error initializing RFID reader: {e}") + log_info_with_server(f"Critical RFID error: {str(e)}") + print("Application will start but RFID functionality will be disabled") +else: + rfid_reader = None + print("🚫 Card presence disabled – RFID reader NOT started") + log_info_with_server("Card presence is disabled – RFID reader skipped, running without card reader")