├── LICENSE ├── Descargando ├── java.desktop ├── java_16.png ├── java_48.png ├── ssl └── empty ├── jconsole.desktop ├── jshell.desktop ├── src ├── fiscalberry │ ├── ui │ │ ├── __init__.py │ │ ├── assets │ │ │ ├── bg.jpg │ │ │ ├── lion.ico │ │ │ ├── lion.jpg │ │ │ ├── lion.png │ │ │ ├── play.png │ │ │ ├── stop.png │ │ │ ├── connected.png │ │ │ ├── fiscalberry.ico │ │ │ ├── fiscalberry.jpg │ │ │ ├── fiscalberry.png │ │ │ ├── disconnected.png │ │ │ ├── stop.svg │ │ │ └── play.svg │ │ ├── adopt_screen.py │ │ ├── main_screen.py │ │ ├── kv │ │ │ ├── fiscalberry.kv │ │ │ └── main.kv │ │ ├── log_screen.py │ │ ├── login_screen.py │ │ ├── old_fiscalberry_app.py │ │ └── fiscalberry_app.py │ ├── version.py │ ├── common │ │ ├── __init__.py │ │ ├── rabbitmq │ │ │ ├── __init__.py │ │ │ ├── consumer.py │ │ │ ├── process_handler.py.backup │ │ │ └── process_handler.py │ │ ├── afip.bmp │ │ ├── assets │ │ │ └── fiscalberry.ico │ │ ├── printer_detector.py │ │ ├── FiscalberryComandos.py │ │ ├── discover.py │ │ ├── token_manager.py │ │ ├── fiscalberry_sio.py │ │ ├── fiscalberry_logger.py │ │ └── service_controller.py │ ├── gui.py │ ├── __init__.py │ ├── cli.py │ └── diagnostics │ │ ├── test_error_handling.py │ │ └── rabbitmq_check.py ├── .env.example ├── config.ini.install ├── main.py ├── config.ini.sample ├── scripts │ └── check_internet.py ├── fiscalberryservice │ └── android.py ├── test.py └── LICENSE ├── server.bat ├── assets └── paxapos.ico ├── my_recipes └── jpeg │ ├── Application.mk │ ├── remove-version.patch │ ├── build-static.patch │ └── __init__.py ├── requirements.android.txt ├── requirements.txt ├── requirements.cli.txt ├── fiscalberry.service ├── requirements.kivy.txt ├── test_mode_detection.py ├── Dockerfile ├── Dockerfile.standalone ├── Dockerfile.build ├── fiscalberry-cli.spec ├── file_version_info.txt ├── fiscalberry-gui.spec ├── setup.py ├── .devcontainer └── devcontainer.json ├── venv.cli └── share │ └── man │ └── man1 │ └── qr.1 ├── .gitignore ├── .github └── workflows │ ├── android.build.yml │ └── desktop.build.yml ├── demo_priority_logic.py ├── docs ├── logica_prioridad_rabbitmq.md ├── rabbitmq_error_handling.md └── mejoras_rabbitmq_resumen.md ├── test_config_priority.py └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Descargando: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /java.desktop: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /java_16.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /java_48.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ssl/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jconsole.desktop: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jshell.desktop: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | ENVIRONMENT="development" -------------------------------------------------------------------------------- /src/fiscalberry/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "2.0.1" -------------------------------------------------------------------------------- /server.bat: -------------------------------------------------------------------------------- 1 | start "" "pythonw" "C:\fiscalberry\server.py" -------------------------------------------------------------------------------- /src/fiscalberry/common/__init__.py: -------------------------------------------------------------------------------- 1 | # archivo init del modulo common -------------------------------------------------------------------------------- /src/fiscalberry/common/rabbitmq/__init__.py: -------------------------------------------------------------------------------- 1 | # archivo init del modulo common -------------------------------------------------------------------------------- /assets/paxapos.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/assets/paxapos.ico -------------------------------------------------------------------------------- /src/fiscalberry/common/afip.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/common/afip.bmp -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/bg.jpg -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/lion.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.ico -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/lion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.jpg -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/lion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/lion.png -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/play.png -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/stop.png -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/connected.png -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/fiscalberry.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.ico -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/fiscalberry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.jpg -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/fiscalberry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/fiscalberry.png -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/ui/assets/disconnected.png -------------------------------------------------------------------------------- /src/fiscalberry/common/assets/fiscalberry.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paxapos/fiscalberry/HEAD/src/fiscalberry/common/assets/fiscalberry.ico -------------------------------------------------------------------------------- /my_recipes/jpeg/Application.mk: -------------------------------------------------------------------------------- 1 | APP_OPTIM := release 2 | APP_ABI := all # or armeabi 3 | APP_MODULES := libjpeg 4 | APP_ALLOW_MISSING_DEPS := true 5 | -------------------------------------------------------------------------------- /requirements.android.txt: -------------------------------------------------------------------------------- 1 | python-escpos[image,qrcode,usb,serial]==3.1 2 | python-socketio[client]==5.11.4 3 | requests==2.32.3 4 | platformdirs==4.2.2 5 | pika 6 | kivy[base]==2.3.1 -------------------------------------------------------------------------------- /src/fiscalberry/gui.py: -------------------------------------------------------------------------------- 1 | from fiscalberry.ui.fiscalberry_app import FiscalberryApp 2 | 3 | 4 | def main(): 5 | FiscalberryApp().run() 6 | 7 | if __name__ == "__main__": 8 | main() -------------------------------------------------------------------------------- /src/fiscalberry/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | 4 | """ 5 | Fiscalberry Server Package 6 | """ 7 | from fiscalberry.version import VERSION 8 | 9 | __version__ = VERSION -------------------------------------------------------------------------------- /src/config.ini.install: -------------------------------------------------------------------------------- 1 | [SERVIDOR] 2 | puerto = 12000 3 | discover_url = 4 | ssl_crt_path = 5 | ssl_key_path = 6 | uuid = 7 | 8 | [IMPRESORA_FISCAL] 9 | marca = Hasar 10 | modelo = 715v2 11 | path = /tmp/respuestas.txt 12 | driver = File 13 | 14 | [IMPRESORA_RED] 15 | marca = EscP 16 | host = 127.0.0.1 17 | driver = ReceiptDirectJet 18 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # src/main.py 2 | import runpy 3 | import os 4 | 5 | # Set the current working directory to the script's directory 6 | # This helps ensure relative paths within your app work correctly 7 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | # Run your actual main script 10 | runpy.run_module('fiscalberry.gui', run_name='__main__') -------------------------------------------------------------------------------- /src/config.ini.sample: -------------------------------------------------------------------------------- 1 | [SERVIDOR] 2 | uuid = xxxxx-yyyy-zzzz-aaaa-zzzzzzzzzz 3 | sio_host = http://localhost:5000 4 | sio_password = 5 | 6 | 7 | 8 | [IMPRESORA_FISCAL] 9 | marca = Hasar 10 | modelo = 715v2 11 | path = /tmp/respuestas.txt 12 | driver = File 13 | 14 | [IMPRESORA_RED] 15 | marca = EscP 16 | host = 127.0.0.1 17 | driver = ReceiptDirectJet 18 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Fiscalberry CLI Dependencies 2 | # Última actualización: 2025-07-19 3 | # Usa las últimas versiones estables de todas las dependencias 4 | 5 | # Core dependencies 6 | python-escpos[all] 7 | python-socketio[client] 8 | requests 9 | aiohttp 10 | 11 | # Utility libraries 12 | appdirs 13 | platformdirs 14 | 15 | # Message broker 16 | pika 17 | 18 | # Windows-specific dependencies 19 | pywin32; sys_platform == 'win32' -------------------------------------------------------------------------------- /requirements.cli.txt: -------------------------------------------------------------------------------- 1 | # Fiscalberry CLI Dependencies 2 | # Última actualización: 2025-07-19 3 | # Usa las últimas versiones estables de todas las dependencias 4 | 5 | # Core dependencies 6 | python-escpos[all] 7 | python-socketio[client] 8 | requests 9 | aiohttp 10 | 11 | # Utility libraries 12 | appdirs 13 | platformdirs 14 | 15 | # Message broker 16 | pika 17 | 18 | # Windows-specific dependencies 19 | pywin32; sys_platform == 'win32' -------------------------------------------------------------------------------- /fiscalberry.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fiscalberry Server 3 | Wants=network-online.target 4 | After=network.target network-online.target 5 | 6 | [Service] 7 | User=root 8 | Group=root 9 | Restart=always 10 | RestartSec=5 11 | Type=simple 12 | ExecStart=/usr/bin/fiscalberry 13 | StandardOutput=syslog 14 | StandardError=syslog 15 | SyslogIdentifier=fiscalberry 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | Alias=fiscalberry.service 20 | -------------------------------------------------------------------------------- /requirements.kivy.txt: -------------------------------------------------------------------------------- 1 | # Fiscalberry GUI (Kivy) Dependencies 2 | # Última actualización: 2025-07-19 3 | # Usa las últimas versiones estables de todas las dependencias 4 | 5 | # GUI Framework 6 | kivy[base] 7 | 8 | # Core dependencies (from CLI) 9 | python-escpos[all] 10 | python-socketio[client] 11 | requests 12 | aiohttp 13 | 14 | # Utility libraries 15 | appdirs 16 | platformdirs 17 | 18 | # Message broker 19 | pika 20 | 21 | # Windows-specific dependencies 22 | pywin32; sys_platform == 'win32' -------------------------------------------------------------------------------- /src/fiscalberry/ui/assets/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scripts/check_internet.py: -------------------------------------------------------------------------------- 1 | # /opt/fiscalberryNUEVO/scripts/check_internet.py 2 | import socket 3 | import sys 4 | 5 | def check_internet(host="8.8.8.8", port=53, timeout=5): 6 | try: 7 | socket.setdefaulttimeout(timeout) 8 | socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) 9 | return True 10 | except socket.error as ex: 11 | print(f"Error checking internet connection: {ex}") 12 | return False 13 | 14 | if not check_internet(): 15 | sys.exit(1) # Exit with non-zero status to prevent the service from starting -------------------------------------------------------------------------------- /my_recipes/jpeg/remove-version.patch: -------------------------------------------------------------------------------- 1 | --- jpeg/CMakeLists.txt.orig 2018-11-12 20:20:28.000000000 +0100 2 | +++ jpeg/CMakeLists.txt 2018-12-14 12:43:45.338704504 +0100 3 | @@ -573,6 +573,9 @@ 4 | add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES}) 5 | set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS 6 | "-DBMP_SUPPORTED -DPPM_SUPPORTED") 7 | + set_property(TARGET jpeg PROPERTY NO_SONAME 1) 8 | + set_property(TARGET turbojpeg PROPERTY NO_SONAME 1) 9 | + set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "") 10 | if(WIN32) 11 | set_target_properties(turbojpeg PROPERTIES DEFINE_SYMBOL DLLDEFINE) 12 | endif() 13 | -------------------------------------------------------------------------------- /src/fiscalberry/common/printer_detector.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | def listar_impresoras(): 4 | sistema_operativo = platform.system() 5 | impresoras = [] 6 | 7 | if sistema_operativo == "Linux": 8 | import subprocess 9 | 10 | resultado = subprocess.run(['lpstat', '-e'], stdout=subprocess.PIPE) 11 | impresoras = resultado.stdout.decode('utf-8').splitlines() 12 | elif sistema_operativo == "Windows": 13 | import win32print 14 | 15 | impresoras = [printer[2] for printer in win32print.EnumPrinters(2)] 16 | else: 17 | raise NotImplementedError("Sistema operativo no soportado") 18 | 19 | return impresoras -------------------------------------------------------------------------------- /test_mode_detection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | # Agregar el directorio src al path 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) 7 | 8 | from fiscalberry.common.service_controller import ServiceController 9 | 10 | def test_mode_detection(): 11 | controller = ServiceController() 12 | 13 | print("Probando detección de modo...") 14 | is_gui = controller._is_gui_mode() 15 | print(f"¿Es modo GUI? {is_gui}") 16 | 17 | if is_gui: 18 | print("Modo GUI detectado - usará stop_for_gui()") 19 | else: 20 | print("Modo CLI detectado - usará stop_for_cli()") 21 | 22 | if __name__ == "__main__": 23 | test_mode_detection() 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim-bullseye 2 | 3 | WORKDIR /usr/src/app 4 | EXPOSE 12000 5 | 6 | 7 | COPY requirements.txt ./ 8 | COPY . . 9 | RUN ls 10 | RUN pip install -r requirements.txt 11 | 12 | 13 | CMD [ "python", "server.py" ] 14 | 15 | ENV CONFIG_FILE_PATH="./" 16 | 17 | 18 | # luego buildear la imagen esta 19 | # docker build -t fiscalberry2 . 20 | 21 | #para buildear asw 22 | # docker build -t yoalevilar/fiscalberry:3 . --no-cache 23 | 24 | # y ejecutarlo asi: 25 | # en la carpeta openvpn guardar todos los archivos de config para conectar con openvpn server 26 | # docker run -d --name fiscalberry --privileged --env=CONFIG_FILE_PATH=/fiscalberry --volume=C:\fiscalberry:/fiscalberry -p 12000:12000 --restart=always yoalevilar/fiscalberry:3 -------------------------------------------------------------------------------- /Dockerfile.standalone: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | # Instalar dependencias necesarias 4 | RUN apt-get update && apt-get install -y python3 libstdc++6 cups-client 5 | 6 | RUN apt-get update && apt-get install -y wget 7 | 8 | RUN wget https://github.com/paxapos/fiscalberry/releases/download/v1.0.20/fiscalberry-lin -O /usr/local/bin/fiscalberry-lin 9 | 10 | RUN chmod +x /usr/local/bin/fiscalberry-lin 11 | 12 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 13 | CMD ["/usr/local/bin/fiscalberry-lin"] 14 | 15 | 16 | # rememter to create volume for config file in /root/.config/Fiscalberry/config.ini 17 | # echo docker run -d --name fiscalberry --privileged --volume=/home/$USER/.config/Fiscalberry:/root/.config/Fiscalberry --restart=always fiscalberry-standalone 18 | -------------------------------------------------------------------------------- /src/fiscalberry/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | from fiscalberry.common.fiscalberry_logger import getLogger 5 | logger = getLogger() 6 | from fiscalberry.common.service_controller import ServiceController 7 | 8 | def main(): 9 | """Función principal que ejecuta el controlador de servicios.""" 10 | controller = ServiceController() 11 | 12 | try: 13 | controller.start() 14 | except KeyboardInterrupt: 15 | print("\nInterrupción de teclado detectada. Cerrando servicios...") 16 | controller.stop() 17 | print("Servicio detenido. Saliendo.") 18 | except Exception as e: 19 | print(f"Error inesperado: {e}") 20 | controller.stop() 21 | sys.exit(1) 22 | 23 | if __name__ == "__main__": 24 | main() 25 | 26 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM python:3-slim-bullseye 2 | 3 | WORKDIR /usr/src/app 4 | EXPOSE 12000 5 | 6 | 7 | COPY requirements.cli.txt ./ 8 | COPY . . 9 | RUN ls 10 | 11 | RUN apt-get update && \ 12 | apt-get install -y libcups2-dev && \ 13 | python3 -m pip install --upgrade pip && \ 14 | pip3 install pyinstaller && \ 15 | pip3 install -r requirements.cli.txt 16 | 17 | # luego buildear la imagen esta 18 | # docker build -t fiscalberry2 . 19 | 20 | #para buildear asw 21 | # docker build -t yoalevilar/fiscalberry:3 . --no-cache 22 | 23 | # y ejecutarlo asi: 24 | # en la carpeta openvpn guardar todos los archivos de config para conectar con openvpn server 25 | # docker run -d --name fiscalberry --privileged --env=CONFIG_FILE_PATH=/fiscalberry --volume=C:\fiscalberry:/fiscalberry -p 12000:12000 --restart=always yoalevilar/fiscalberry:3 -------------------------------------------------------------------------------- /src/fiscalberry/ui/adopt_screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | from kivy.uix.screenmanager import Screen 5 | from kivy.app import App 6 | import tempfile 7 | from fiscalberry.common.token_manager import save_token, do_login 8 | from fiscalberry.common.Configberry import Configberry 9 | from fiscalberry.common.fiscalberry_logger import getLogger 10 | from kivy.properties import StringProperty 11 | 12 | logger = getLogger() 13 | 14 | configberry = Configberry() 15 | host = configberry.get("SERVIDOR", "sio_host", "https://beta.paxapos.com") 16 | uuid = configberry.get("SERVIDOR", "uuid", fallback="") 17 | 18 | QRGENLINK = "https://codegenerator.paxapos.com/?bcid=qrcode&text=" 19 | ADOP_LINK = host + "/adopt/" + uuid 20 | class AdoptScreen(Screen): 21 | adoptarLink = StringProperty(ADOP_LINK) 22 | qrCodeLink = StringProperty(QRGENLINK + ADOP_LINK) 23 | 24 | 25 | -------------------------------------------------------------------------------- /fiscalberry-cli.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['src/fiscalberry/cli.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[('./capabilities.json', 'escpos')], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.datas, 24 | [], 25 | name='fiscalberry-cli', 26 | debug=False, 27 | bootloader_ignore_signals=False, 28 | strip=False, 29 | upx=True, 30 | upx_exclude=[], 31 | runtime_tmpdir=None, 32 | console=True, 33 | disable_windowed_traceback=False, 34 | argv_emulation=False, 35 | target_arch=None, 36 | codesign_identity=None, 37 | entitlements_file=None, 38 | icon=['src/fiscalberry/ui/assets/fiscalberry.ico'], 39 | ) 40 | -------------------------------------------------------------------------------- /file_version_info.txt: -------------------------------------------------------------------------------- 1 | # file_version_info.txt 2 | VSVersionInfo( 3 | ffi=FixedFileInfo( 4 | filevers=(1, 0, 0, 0), 5 | prodvers=(1, 0, 0, 0), 6 | mask=0x3f, 7 | flags=0x0, 8 | OS=0x40004, 9 | fileType=0x1, 10 | subtype=0x0, 11 | date=(0, 0) 12 | ), 13 | kids=[ 14 | StringFileInfo( 15 | [ 16 | StringTable( 17 | u'040904B0', 18 | [StringStruct(u'CompanyName', u'Plus Abstracta'), 19 | StringStruct(u'FileDescription', u'Servidor de Impresion GUI Application'), 20 | StringStruct(u'FileVersion', u'2.0.1.0'), 21 | StringStruct(u'InternalName', u'fiscalberry'), 22 | StringStruct(u'LegalCopyright', u'© Plus Abstracta SRL. All rights reserved.'), 23 | StringStruct(u'OriginalFilename', u'fiscalberry-gui.exe'), 24 | StringStruct(u'ProductName', u'Fiscalberry'), 25 | StringStruct(u'ProductVersion', u'1.0.0.0')]) 26 | ]), 27 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) 28 | ] 29 | ) -------------------------------------------------------------------------------- /fiscalberry-gui.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['src/fiscalberry/gui.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[ 9 | ('./capabilities.json', 'escpos'), 10 | ('src/fiscalberry/ui/kv', 'fiscalberry/ui/kv') 11 | ], 12 | hiddenimports=[], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=[], 16 | excludes=[], 17 | noarchive=False, 18 | optimize=0, 19 | ) 20 | pyz = PYZ(a.pure) 21 | 22 | exe = EXE( 23 | pyz, 24 | a.scripts, 25 | a.binaries, 26 | a.datas, 27 | [], 28 | name='fiscalberry-gui', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=False, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=False, 36 | disable_windowed_traceback=False, 37 | argv_emulation=False, 38 | target_arch=None, 39 | codesign_identity=None, 40 | entitlements_file=None, 41 | icon=['src/fiscalberry/ui/assets/fiscalberry.ico'], 42 | version='file_version_info.txt', # Add a version file (see below) 43 | uac_admin=False, # Don't request admin privileges unless necessary 44 | ) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | def load_requirements(filename): 5 | with open(filename, "r") as req_file: 6 | return [line.strip() for line in req_file 7 | if line.strip() and not line.startswith("#")] 8 | 9 | 10 | setup( 11 | name='fiscalberry', 12 | version='2.0.0', 13 | description='Proyecto con interfaz Kivy y consola', 14 | author='Ale Vilar', 15 | author_email='alevilar@gmail.com', 16 | package_dir={'': 'src'}, # Le decimos que los paquetes están en "src" 17 | packages=find_packages(where='src'), # Esto encontrará "fiscalberry" 18 | install_requires=load_requirements("requirements.txt"), # Asume que load_requirements está fijo para tomar un nombre de archivo 19 | extras_require={ 20 | 'cli': load_requirements("requirements.cli.txt"), # Assumes load_requirements is fixed to take a filename 21 | 'gui': load_requirements("requirements.kivy.txt"), # Assumes load_requirements is fixed to take a filename 22 | }, 23 | entry_points={ 24 | 'console_scripts': [ 25 | 'fiscalberry_cli=fiscalberry.cli:main', 26 | 'fiscalberry_gui=fiscalberry.gui:main', 27 | ], 28 | }, 29 | ) -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | "forwardPorts": [12000], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | "postCreateCommand": "pip3 install --user -r requirements.txt", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | "remoteUser": "root", 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "KevinRose.vsc-python-indent", 26 | "mgesbert.python-path", 27 | "demystifying-javascript.python-extensions-pack", 28 | "BattleBas.kivy-vscode" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/fiscalberry/common/FiscalberryComandos.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from fiscalberry.common.fiscalberry_logger import getLogger 4 | 5 | class PrinterException(Exception): 6 | pass 7 | 8 | logger = getLogger() 9 | 10 | class FiscalberryComandos(): 11 | 12 | timeout = 5 13 | 14 | DEFAULT_DRIVER = "Fiscalberry" 15 | 16 | def run(self, host, jsonData): 17 | """Envia comando a impresora""" 18 | 19 | logger.info(f"Conectando a la URL {host}") 20 | headers = {'Content-type': 'application/json'} 21 | 22 | try: 23 | reply = requests.post(host, data=json.dumps(jsonData), headers=headers, timeout=self.timeout) 24 | 25 | if reply.status_code == 200: 26 | logger.info("Conexión exitosa") 27 | return [] 28 | except requests.exceptions.Timeout as e: 29 | # Maybe set up for a retry, or continue in a retry loop 30 | # Sin timeout, si NO existe la IP de destino, queda intentando infinitamente. 31 | logger.error(f"Timeout de conexión con {host} después de {self.timeout} segundos") 32 | return [] 33 | 34 | except requests.exceptions.RequestException as e: 35 | # catastrophic error. bail. 36 | # Raises en caso de que exista la IP destino pero el puerto esté cerrado 37 | logger.error(f"No se pudo establecer la conexión con {host}") 38 | return [] 39 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/main_screen.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.screenmanager import Screen 2 | from kivy.app import App # Import the App class 3 | import os 4 | from fiscalberry.common.token_manager import delete_token 5 | from kivy.properties import StringProperty 6 | from os.path import join, dirname 7 | 8 | class MainScreen(Screen): 9 | stop_image = StringProperty(join(dirname(__file__), "assets/stop.png")) 10 | play_image = StringProperty(join(dirname(__file__), "assets/play.png")) 11 | 12 | connected_image = StringProperty(join(dirname(__file__), "assets/connected.png")) 13 | disconnected_image = StringProperty(join(dirname(__file__), "assets/disconnected.png")) 14 | 15 | def start_service(self): 16 | """Inicia el servicio desde la GUI.""" 17 | app = App.get_running_app() # Use App.get_running_app() 18 | app.on_start_service() 19 | 20 | def toggle_service(self): 21 | """Alterna el estado del servicio desde la GUI.""" 22 | app = App.get_running_app() 23 | app.on_toggle_service() 24 | 25 | def stop_service(self): 26 | """Detiene el servicio desde la GUI.""" 27 | app = App.get_running_app() # Use App.get_running_app() 28 | app.on_stop_service() 29 | 30 | def logout(self): 31 | """Cierra sesión y elimina el token JWT.""" 32 | delete_token() 33 | 34 | app = App.get_running_app() # Use App.get_running_app() 35 | app.root.current = "login" -------------------------------------------------------------------------------- /venv.cli/share/man/man1/qr.1: -------------------------------------------------------------------------------- 1 | .\" Manpage for qr 2 | .TH QR 1 "6 Feb 2023" "7.4.2" "Python QR tool" 3 | .SH NAME 4 | qr \- script to create QR codes at the command line 5 | .SH SYNOPSIS 6 | qr [\-\-help] [\-\-factory=FACTORY] [\-\-optimize=OPTIMIZE] [\-\-error\-correction=LEVEL] [data] 7 | .SH DESCRIPTION 8 | This script uses the python qrcode module. It can take data from stdin or from the commandline and generate a QR code. 9 | Normally it will output the QR code as ascii art to the terminal. If the output is piped to a file, it will output the image (default type of PNG). 10 | .SH OPTIONS 11 | .PP 12 | \fB\ \-h, \-\-help\fR 13 | .RS 4 14 | Show a help message. 15 | .RE 16 | 17 | .PP 18 | \fB\ \-\-factory=FACTORY\fR 19 | .RS 4 20 | Full python path to the image factory class to create the 21 | image with. You can use the following shortcuts to the 22 | built-in image factory classes: pil (default), png ( 23 | default if pillow is not installed), svg, svg-fragment, 24 | svg-path. 25 | .RE 26 | 27 | .PP 28 | \fB\ \-\-optimize=OPTIMIZE\fR 29 | .RS 4 30 | Optimize the data by looking for chunks of at least this 31 | many characters that could use a more efficient encoding 32 | method. Use 0 to turn off chunk optimization. 33 | .RE 34 | 35 | .PP 36 | \fB\ \-\-error\-correction=LEVEL\fR 37 | .RS 4 38 | The error correction level to use. Choices are L (7%), 39 | M (15%, default), Q (25%), and H (30%). 40 | .RE 41 | 42 | .PP 43 | \fB\ data\fR 44 | .RS 4 45 | The data from which the QR code will be generated. 46 | .RE 47 | 48 | .SH SEE ALSO 49 | https://github.com/lincolnloop/python-qrcode/ 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | .pyc 7 | *.log 8 | *.swp 9 | py3env 10 | bin 11 | lib 12 | lib64 13 | output 14 | .buildozer 15 | 16 | #configuration file for fiscalberry 17 | config.ini 18 | bemafix.py 19 | fiscaltest.py 20 | tornado.* 21 | comanderatest.py 22 | test_scripts/* 23 | env/* 24 | ssl/* 25 | tmp/* 26 | .venv 27 | venv.kivy/ 28 | .venv.android/ 29 | 30 | # Distribution / packaging 31 | *.pyc 32 | .Python 33 | env/ 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *,cover 67 | .hypothesis/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # IPython Notebook 91 | .ipynb_checkpoints 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # dotenv 100 | .env 101 | 102 | # virtualenv 103 | venv/ 104 | ENV/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | 113 | .vscode/ 114 | fiscalberry3env 115 | pyvenv.cfg 116 | 117 | 118 | kivy_venv 119 | fiscalberry3env 120 | pyManjaroEnv 121 | bin 122 | build 123 | -------------------------------------------------------------------------------- /src/fiscalberry/common/discover.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import requests 3 | import json 4 | from fiscalberry.common.Configberry import Configberry 5 | from fiscalberry.common.fiscalberry_logger import getLogger 6 | from fiscalberry.common.printer_detector import listar_impresoras 7 | 8 | configberry = Configberry() 9 | 10 | def send_discover(): 11 | 12 | logger = getLogger() 13 | discoverUrl = False 14 | 15 | uuidval = configberry.config.get("SERVIDOR", "uuid", fallback="") 16 | 17 | if not uuidval: 18 | logger.error("No se ha configurado el uuid en el archivo de configuracion") 19 | return None 20 | 21 | data = configberry.getJSON() 22 | data["installed_printers"] = listar_impresoras() 23 | senddata = { 24 | "uuid": configberry.config.get("SERVIDOR", "uuid"), 25 | "raw_data": json.dumps(data) 26 | } 27 | 28 | ### Enviar al Discover 29 | host = configberry.config.get("SERVIDOR", "sio_host", fallback="") 30 | 31 | discoverUrl = host + "/discover.json" 32 | logger.info(f"DISCOVER:: URL: {discoverUrl}") 33 | ret = None 34 | 35 | if discoverUrl: 36 | try: 37 | headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} 38 | ret = requests.post(discoverUrl, headers=headers, data=json.dumps(senddata)) 39 | 40 | if ret.status_code != requests.codes.ok: 41 | raise Exception(f"Error de conexión con {discoverUrl} status_code: {ret.status_code} - {ret.text}") 42 | 43 | except Exception as e: 44 | if str(e.args[0]).startswith("Invalid URL"): logger.error(f"El formato de 'discover_url' es inválido: \033[91m{discoverUrl}\033[0m") 45 | 46 | logger.error(f"No es posible conectarse con el Discover en {discoverUrl}. El error dice: {str(e)}") 47 | 48 | else: 49 | logger.info("No hay sio_host configurado, no tengo el host donde hacer el discover") 50 | 51 | return None 52 | 53 | 54 | def send_discover_in_thread(): 55 | thread = threading.Thread(target=send_discover, daemon=True) 56 | return thread -------------------------------------------------------------------------------- /src/fiscalberry/ui/kv/fiscalberry.kv: -------------------------------------------------------------------------------- 1 | #:kivy 2.3.0 2 | 3 | 4 | : 5 | title: "Fiscalberry App Iupi!" 6 | orientation: 'vertical' 7 | 8 | canvas.before: 9 | Rectangle: 10 | pos: self.pos 11 | size: self.size 12 | source: app.background_image 13 | 14 | 15 | BoxLayout: 16 | orientation: 'vertical' 17 | padding: 10 18 | 19 | Image: 20 | source: app.logo_image 21 | size_hint_y: None 22 | height: 96 23 | allow_stretch: True 24 | keep_ratio: True 25 | color: 1, 1, 1, 1 26 | margin_bottom: 10 27 | 28 | Label: 29 | id: title_label 30 | text: "Print Server" 31 | font_size: 32 32 | bold: True 33 | size_hint_y: None 34 | height: 64 35 | color: 0, 0, 128, 1 36 | margin_bottom: 10 37 | 38 | 39 | Label: 40 | id: conectado_label 41 | text: "Conectado!" if app.connected else "Desconectado" 42 | color: (0, 128, 0, 1) if app.connected else (128, 0, 0, 1) 43 | font_size: 48 44 | 45 | Button: 46 | id: connect_button 47 | text: "Conectar" if not app.connected else "Desconectar" 48 | on_press: app.start_service() if not app.connected else app.stop_service() 49 | size_hint_y: None 50 | height: 64 51 | background_color: (0, 128, 0, 1) if app.connected else (128, 0, 0, 1) 52 | color: 1, 1, 1, 1 53 | 54 | 55 | Label: 56 | id: uuid_label 57 | text: app.uuidSmall 58 | font_size: 16 59 | size_hint_y: None 60 | height: 40 61 | color: 128, 0, 0, 1 62 | 63 | 64 | TextInput: 65 | id: host_input 66 | text: app.sioServerUrl 67 | multiline: False 68 | size_hint_y: None 69 | height: 40 70 | on_text: app.store_new_host(self.text) 71 | 72 | 73 | 74 | : 75 | height: 200 76 | 77 | 78 | : 79 | source: app.connected_image if app.connected else app.disconnected_image 80 | height: 16 81 | keep_ratio: True -------------------------------------------------------------------------------- /my_recipes/jpeg/build-static.patch: -------------------------------------------------------------------------------- 1 | diff -Naur jpeg/Android.mk b/Android.mk 2 | --- jpeg/Android.mk 2015-12-14 11:37:25.900190235 -0600 3 | +++ b/Android.mk 2015-12-14 11:41:27.532182210 -0600 4 | @@ -54,8 +54,7 @@ 5 | 6 | LOCAL_SRC_FILES:= $(libjpeg_SOURCES_DIST) 7 | 8 | -LOCAL_SHARED_LIBRARIES := libcutils 9 | -LOCAL_STATIC_LIBRARIES := libsimd 10 | +LOCAL_STATIC_LIBRARIES := libsimd libcutils 11 | 12 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 13 | 14 | @@ -68,7 +67,7 @@ 15 | 16 | LOCAL_MODULE := libjpeg 17 | 18 | -include $(BUILD_SHARED_LIBRARY) 19 | +include $(BUILD_STATIC_LIBRARY) 20 | 21 | ###################################################### 22 | ### cjpeg ### 23 | @@ -82,7 +81,7 @@ 24 | 25 | LOCAL_SRC_FILES:= $(cjpeg_SOURCES) 26 | 27 | -LOCAL_SHARED_LIBRARIES := libjpeg 28 | +LOCAL_STATIC_LIBRARIES := libjpeg 29 | 30 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \ 31 | $(LOCAL_PATH)/android 32 | @@ -110,7 +109,7 @@ 33 | 34 | LOCAL_SRC_FILES:= $(djpeg_SOURCES) 35 | 36 | -LOCAL_SHARED_LIBRARIES := libjpeg 37 | +LOCAL_STATIC_LIBRARIES := libjpeg 38 | 39 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \ 40 | $(LOCAL_PATH)/android 41 | @@ -137,7 +136,7 @@ 42 | 43 | LOCAL_SRC_FILES:= $(jpegtran_SOURCES) 44 | 45 | -LOCAL_SHARED_LIBRARIES := libjpeg 46 | +LOCAL_STATIC_LIBRARIES := libjpeg 47 | 48 | LOCAL_C_INCLUDES := $(LOCAL_PATH) \ 49 | $(LOCAL_PATH)/android 50 | @@ -163,7 +162,7 @@ 51 | 52 | LOCAL_SRC_FILES:= $(tjunittest_SOURCES) 53 | 54 | -LOCAL_SHARED_LIBRARIES := libjpeg 55 | +LOCAL_STATIC_LIBRARIES := libjpeg 56 | 57 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 58 | 59 | @@ -189,7 +188,7 @@ 60 | 61 | LOCAL_SRC_FILES:= $(tjbench_SOURCES) 62 | 63 | -LOCAL_SHARED_LIBRARIES := libjpeg 64 | +LOCAL_STATIC_LIBRARIES := libjpeg 65 | 66 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 67 | 68 | @@ -215,7 +214,7 @@ 69 | 70 | LOCAL_SRC_FILES:= $(rdjpgcom_SOURCES) 71 | 72 | -LOCAL_SHARED_LIBRARIES := libjpeg 73 | +LOCAL_STATIC_LIBRARIES := libjpeg 74 | 75 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 76 | 77 | @@ -240,7 +239,7 @@ 78 | 79 | LOCAL_SRC_FILES:= $(wrjpgcom_SOURCES) 80 | 81 | -LOCAL_SHARED_LIBRARIES := libjpeg 82 | +LOCAL_STATIC_LIBRARIES := libjpeg 83 | 84 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 85 | 86 | -------------------------------------------------------------------------------- /my_recipes/jpeg/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import Recipe 2 | from pythonforandroid.logger import shprint 3 | from pythonforandroid.util import current_directory 4 | from os.path import join 5 | import sh 6 | 7 | 8 | class JpegRecipe(Recipe): 9 | ''' 10 | .. versionchanged:: 0.6.0 11 | rewrote recipe to be build with clang and updated libraries to latest 12 | version of the official git repo. 13 | ''' 14 | name = 'jpeg' 15 | version = '2.0.1' 16 | url = 'https://github.com/libjpeg-turbo/libjpeg-turbo/archive/{version}.tar.gz' # noqa 17 | built_libraries = {'libjpeg.a': '.', 'libturbojpeg.a': '.'} 18 | # we will require this below patch to build the shared library 19 | # patches = ['remove-version.patch'] 20 | 21 | def build_arch(self, arch): 22 | build_dir = self.get_build_dir(arch.arch) 23 | 24 | # TODO: Fix simd/neon 25 | with current_directory(build_dir): 26 | env = self.get_recipe_env(arch) 27 | toolchain_file = join(self.ctx.ndk_dir, 28 | 'build/cmake/android.toolchain.cmake') 29 | 30 | shprint(sh.rm, '-rf', 'CMakeCache.txt', 'CMakeFiles/') 31 | shprint(sh.cmake, '-G', 'Unix Makefiles', 32 | '-DCMAKE_SYSTEM_NAME=Android', 33 | '-DCMAKE_POSITION_INDEPENDENT_CODE=1', 34 | '-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch), 35 | '-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir, 36 | '-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()), 37 | '-DCMAKE_CXX_COMPILER={cc_plus}'.format( 38 | cc_plus=arch.get_clang_exe(plus_plus=True)), 39 | '-DCMAKE_BUILD_TYPE=Release', 40 | '-DCMAKE_INSTALL_PREFIX=./install', 41 | '-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file, 42 | 43 | '-DANDROID_ABI={arch}'.format(arch=arch.arch), 44 | '-DANDROID_ARM_NEON=ON', 45 | '-DENABLE_NEON=ON', 46 | # '-DREQUIRE_SIMD=1', 47 | 48 | # Force disable shared, with the static ones is enough 49 | '-DENABLE_SHARED=0', 50 | '-DENABLE_STATIC=1', 51 | '-DCMAKE_POLICY_VERSION_MINIMUM=3.5', 52 | 53 | _env=env) 54 | shprint(sh.make, _env=env) 55 | 56 | 57 | recipe = JpegRecipe() 58 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/log_screen.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.screenmanager import Screen 2 | from kivy.properties import StringProperty 3 | from kivy.clock import Clock 4 | from fiscalberry.common.fiscalberry_logger import getLogFilePath 5 | import platform 6 | import os 7 | import subprocess 8 | class LogScreen(Screen): 9 | logs = StringProperty("") # Propiedad para almacenar los logs 10 | logFilePath = StringProperty("") # Propiedad para almacenar la ruta del log 11 | 12 | def on_kv_post(self, base_widget): 13 | # Se garantiza que los ids están disponibles 14 | Clock.schedule_interval(self.update_logs, 1) # Actualizar cada segundo 15 | 16 | 17 | def open_log_file(self): 18 | """Abre el archivo de log en el editor de texto predeterminado.""" 19 | if self.logFilePath: 20 | 21 | try: 22 | system = platform.system() 23 | if system == "Windows": 24 | # os.startfile opens the file with the default application on Windows 25 | os.startfile(self.logFilePath) 26 | elif system == "Darwin": # macOS 27 | subprocess.Popen(["open", self.logFilePath]) 28 | else: # Linux and other Unix-like systems 29 | # xdg-open opens the file with the default application on Linux 30 | subprocess.Popen(["xdg-open", self.logFilePath]) 31 | except FileNotFoundError: 32 | self.logs = f"Error: No se encontró el archivo de log en {self.logFilePath}" 33 | except OSError as e: 34 | # Catch errors like 'xdg-open' or 'open' not found, or no default app associated 35 | self.logs = f"Error al abrir el log con la aplicación predeterminada: {e}" 36 | except Exception as e: 37 | self.logs = f"Error inesperado al abrir el log: {e}" 38 | else: 39 | self.logs = "No se ha encontrado la ruta del archivo de log." 40 | 41 | def update_logs(self, dt): 42 | """Lee el archivo de logs y actualiza la propiedad `logs`.""" 43 | self.logFilePath = getLogFilePath() 44 | 45 | try: 46 | with open(self.logFilePath, "r") as log_file: 47 | log_data = log_file.read() # Leer contenido del log 48 | self.logs = log_data 49 | # Actualizar el scroll si existe el ScrollView 50 | if "scroll_view" in self.ids: 51 | self.ids.scroll_view.scroll_y = 0 52 | except Exception as e: 53 | self.logs = f"Error al leer log: {e}" -------------------------------------------------------------------------------- /src/fiscalberry/ui/login_screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | from kivy.uix.screenmanager import Screen 5 | from kivy.app import App 6 | import tempfile 7 | from fiscalberry.common.token_manager import save_token, do_login 8 | from fiscalberry.common.Configberry import Configberry 9 | from fiscalberry.common.fiscalberry_logger import getLogger 10 | from kivy.properties import StringProperty 11 | 12 | logger = getLogger() 13 | 14 | configberry = Configberry() 15 | host = configberry.get("SERVIDOR", "sio_host", "https://beta.paxapos.com") 16 | uuid = configberry.get("SERVIDOR", "uuid", fallback="") 17 | 18 | QRGENLINK = "https://codegenerator.paxapos.com/?bcid=qrcode&text=" 19 | ADOP_LINK = host + "/adopt/" + uuid 20 | class LoginScreen(Screen): 21 | errorMsg = StringProperty("") 22 | adoptarLink = StringProperty(ADOP_LINK) 23 | qrCodeLink = StringProperty(QRGENLINK + ADOP_LINK) 24 | 25 | 26 | def on_leave(self, *args): 27 | """Se llama automáticamente cuando se sale de la pantalla.""" 28 | self.errorMsg = "" 29 | 30 | def login(self, username, password): 31 | """Autentica al usuario contra el backend.""" 32 | backend_url = host.rstrip("/") + "/login.json" 33 | try: 34 | if do_login(username, password): 35 | # Login exitoso 36 | app = App.get_running_app() 37 | app.root.current = "main" 38 | return True 39 | else: 40 | # Login fallido 41 | logger.error("Error de autenticación.") 42 | return False 43 | except requests.exceptions.RequestException as e: 44 | logger.error(f"Error al conectar con el backend: {e}") 45 | self.errorMsg = f"Error de conexión: {e}" 46 | return False 47 | except Exception as e: 48 | logger.error(f"Error de autenticación: {e}") 49 | self.errorMsg = str(e) 50 | return False 51 | 52 | 53 | 54 | 55 | 56 | 57 | def save_token(self, token): 58 | """Guarda el token JWT en un archivo local.""" 59 | return save_token(token) 60 | 61 | def get_sites(self): 62 | """Obtiene los sitios del usuario autenticado.""" 63 | backend_url = host.rstrip("/") + "/sites.json" 64 | try: 65 | response = requests.get(backend_url, headers={"Authorization": f"Bearer {self.load_token()}"}) 66 | logger.info(f"Response: {response.status_code}, {response.text}") 67 | if response.status_code == 200: 68 | return response.json() 69 | else: 70 | logger.error("Error al obtener los sitios.") 71 | return [] 72 | except requests.exceptions.RequestException as e: 73 | logger.error(f"Error al conectar con el backend: {e}") 74 | return [] 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/fiscalberry/diagnostics/test_error_handling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script de prueba para verificar el comportamiento del manejo de errores mejorado. 4 | """ 5 | 6 | import sys 7 | import os 8 | import threading 9 | import time 10 | 11 | # Agregar el directorio src al path para importar los módulos 12 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) 13 | 14 | from fiscalberry.common.rabbitmq.process_handler import RabbitMQProcessHandler 15 | from fiscalberry.common.Configberry import Configberry 16 | import queue 17 | 18 | def test_error_handling(): 19 | """Prueba el manejo de errores con configuración que causará error DNS.""" 20 | print("=== Prueba del manejo de errores mejorado ===\n") 21 | 22 | # Configurar valores que causarán error DNS 23 | config = Configberry() 24 | 25 | # Guardar configuración original 26 | original_config = { 27 | "host": config.get("RabbitMq", "host"), 28 | "port": config.get("RabbitMq", "port"), 29 | "user": config.get("RabbitMq", "user"), 30 | "password": config.get("RabbitMq", "password"), 31 | "vhost": config.get("RabbitMq", "vhost"), 32 | "queue": config.get("RabbitMq", "queue"), 33 | } 34 | 35 | try: 36 | # Configurar para causar error DNS (hostname inexistente) 37 | test_config = { 38 | "host": "rabbitmq", # Este hostname causará error DNS 39 | "port": "5672", 40 | "user": "guest", 41 | "password": "guest", 42 | "vhost": "/", 43 | "queue": "test-queue" 44 | } 45 | 46 | config.set("RabbitMq", test_config) 47 | print(f"Configuración de prueba establecida: {test_config}") 48 | 49 | # Crear queue para mensajes 50 | message_queue = queue.Queue() 51 | 52 | # Inicializar el manejador de proceso 53 | handler = RabbitMQProcessHandler() 54 | 55 | print("\nIniciando RabbitMQ process handler...") 56 | print("Esperando ver errores DNS mejorados y sistema de backoff...\n") 57 | 58 | # Iniciar el handler 59 | handler.start(message_queue) 60 | 61 | # Esperar un poco para ver algunos intentos de conexión 62 | print("Dejando que el sistema intente conectar por 30 segundos...") 63 | print("Deberías ver mensajes de error más descriptivos y reintentos con backoff.\n") 64 | 65 | time.sleep(30) 66 | 67 | print("\nDeteniendo el handler...") 68 | handler.stop(timeout=2) 69 | print("Handler detenido.") 70 | 71 | except KeyboardInterrupt: 72 | print("\nInterrumpido por usuario.") 73 | handler.stop(timeout=2) 74 | 75 | finally: 76 | # Restaurar configuración original 77 | config.set("RabbitMq", original_config) 78 | print("Configuración original restaurada.") 79 | 80 | if __name__ == "__main__": 81 | test_error_handling() 82 | -------------------------------------------------------------------------------- /src/fiscalberry/common/token_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | import platformdirs 5 | 6 | # Define your app name and author for proper directory structure 7 | APP_NAME = "fiscalberry" 8 | APP_AUTHOR = "fiscalberry" 9 | 10 | # Use platformdirs to get the correct config directory 11 | TOKEN_FILE = os.path.join(platformdirs.user_config_dir(APP_NAME, APP_AUTHOR), "fiscalberry_jwt_token.json") 12 | 13 | # Ensure the directory exists 14 | os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True) 15 | 16 | def do_login(username, password): 17 | """Autentica al usuario contra el backend y guarda el token JWT.""" 18 | 19 | from fiscalberry.common.Configberry import Configberry 20 | 21 | cfg = Configberry() 22 | host = cfg.get("SERVIDOR","sio_host") 23 | backend_url = host.rstrip("/") + "/login.json" 24 | 25 | response = requests.post(backend_url, json={"email": username, "password": password}) 26 | if response.status_code == 200: 27 | # Login exitoso, guardar el token JWT 28 | token = response.json().get("jwt") 29 | if token: 30 | save_token(token) 31 | return token 32 | else: 33 | raise Exception("Error: No se recibió un token.") 34 | elif response.status_code == 401: 35 | error_message = response.json().get("message", f"Error de autenticación 401 (Código {response.status_code}). Verifique sus credenciales.") 36 | raise Exception(error_message) 37 | else: 38 | error_message = response.json().get("message", f"Error de autenticación (Código {response.status_code}). Verifique sus credenciales.") 39 | raise Exception(error_message) 40 | return "" 41 | 42 | 43 | def save_token(token): 44 | """Guarda el token JWT en un archivo local.""" 45 | with open(TOKEN_FILE, "w") as f: 46 | json.dump({"token": token}, f) 47 | 48 | 49 | def get_token(): 50 | """Carga el token JWT desde un archivo local.""" 51 | if os.path.exists(TOKEN_FILE): 52 | with open(TOKEN_FILE, "r") as f: 53 | data = json.load(f) 54 | return data.get("token") 55 | return None 56 | 57 | def delete_token(): 58 | """Elimina el token JWT del archivo local.""" 59 | if os.path.exists(TOKEN_FILE): 60 | os.remove(TOKEN_FILE) 61 | 62 | 63 | 64 | def make_authenticated_request(url, method="GET", data=None): 65 | """Realiza una solicitud autenticada usando el token JWT.""" 66 | token = get_token() 67 | if not token: 68 | raise Exception("No se encontró un token JWT. Inicie sesión primero.") 69 | 70 | headers = {"Authorization": f"Bearer {token}"} 71 | try: 72 | if method == "GET": 73 | response = requests.get(url, headers=headers) 74 | elif method == "POST": 75 | response = requests.post(url, json=data, headers=headers) 76 | else: 77 | raise ValueError("Método HTTP no soportado.") 78 | return response 79 | except requests.exceptions.RequestException as e: 80 | print(f"Error en la solicitud: {e}") 81 | return None -------------------------------------------------------------------------------- /.github/workflows/android.build.yml: -------------------------------------------------------------------------------- 1 | name: Build Android APK 2 | 3 | 4 | on: 5 | push: 6 | tags: # Trigger on tags like v*.*.* 7 | - 'NEVER-MATCH-v*.*.*' 8 | workflow_dispatch: # Allows manual triggering if needed 9 | 10 | 11 | jobs: 12 | build-android: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' # O 'zulu', 'adopt', etc. 23 | java-version: '17' 24 | 25 | - name: Set up Python 3.11 # O la versión que prefieras/necesites 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.11' 29 | 30 | - name: Get pip cache dir 31 | id: pip-cache 32 | run: | 33 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 34 | 35 | - name: Cache pip 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.pip-cache.outputs.dir }} 39 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} # Cambia si tus requisitos están en buildozer.spec 40 | restore-keys: | 41 | ${{ runner.os }}-pip- 42 | 43 | - name: Cache Buildozer global directory 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.buildozer 47 | key: ${{ runner.os }}-buildozer-${{ hashFiles('buildozer.spec') }} # Key based on buildozer.spec changes 48 | restore-keys: | 49 | ${{ runner.os }}-buildozer- 50 | 51 | - name: Install Buildozer system dependencies 52 | run: | 53 | sudo apt-get update 54 | sudo apt-get install -y \ 55 | git zip unzip build-essential python3-pip python3-dev \ 56 | libffi-dev libssl-dev libncurses5-dev zlib1g-dev libsqlite3-dev libbz2-dev \ 57 | autoconf automake libtool libltdl-dev pkg-config \ 58 | cython3 ccache 59 | 60 | - name: Install Python build tools and Cython 61 | run: | 62 | python -m pip install --upgrade pip wheel setuptools 63 | python -m pip install cython 64 | 65 | - name: Install Buildozer 66 | run: | 67 | python -m pip install buildozer 68 | python -m pip install -r requirements.android.txt 69 | 70 | 71 | 72 | - name: Build APK with Buildozer 73 | # Set ACLOCAL_PATH and PKG_CONFIG_PATH explicitly before running buildozer 74 | # This helps autotools find necessary macro and package config files 75 | run: | 76 | export ACLOCAL_PATH="${ACLOCAL_PATH}:/usr/share/aclocal" 77 | export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig" 78 | rm -rf ~/.buildozer 79 | rm -rf ./.buildozer 80 | 81 | buildozer -v android release 82 | 83 | # --- step for debugging --- 84 | - name: List bin directory contents (after build) 85 | if: always() # Run even if previous steps fail, to aid debugging 86 | run: | 87 | echo "Listing contents of ./bin directory:" 88 | ls -lR ./bin || echo "Bin directory not found or empty." 89 | 90 | - name: Upload APK artifact 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: fiscalberry-android-apk-${{ github.ref_name }} 94 | path: bin/*.apk -------------------------------------------------------------------------------- /demo_priority_logic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Demostración de la nueva lógica de prioridad config.ini vs SocketIO 4 | para configuración de RabbitMQ en Fiscalberry. 5 | """ 6 | 7 | import configparser 8 | import os 9 | import sys 10 | 11 | def demonstrate_priority_logic(): 12 | """Demuestra cómo funciona la nueva lógica de prioridad""" 13 | 14 | # Leer el config.ini real del usuario 15 | config_path = os.path.expanduser("~/.config/Fiscalberry/config.ini") 16 | 17 | if not os.path.exists(config_path): 18 | print(f"❌ No se encontró {config_path}") 19 | return 20 | 21 | config = configparser.ConfigParser() 22 | config.read(config_path) 23 | 24 | print("🔧 DEMOSTRACIÓN: Lógica de Prioridad RabbitMQ Config.ini vs SocketIO") 25 | print("=" * 70) 26 | 27 | # Mostrar valores actuales del config.ini 28 | print("\n📄 Valores actuales en config.ini:") 29 | if config.has_section('RabbitMq'): 30 | for key in ['host', 'port', 'user', 'password', 'vhost', 'queue']: 31 | value = config.get('RabbitMq', key, fallback='') 32 | status = "✓ Tiene valor" if value and value.strip() else "❌ Vacío" 33 | print(f" {key}: '{value}' {status}") 34 | else: 35 | print(" ❌ No hay sección [RabbitMq]") 36 | 37 | # Simular un mensaje SocketIO con valores diferentes 38 | print("\n📡 Mensaje SocketIO simulado:") 39 | rabbit_cfg = { 40 | 'host': 'rabbitmq.server.socketio.com', 41 | 'port': '5673', 42 | 'user': 'socketio_user', 43 | 'password': 'socketio_password', 44 | 'vhost': '/socketio_vhost', 45 | 'queue': 'socketio_queue' 46 | } 47 | 48 | for key, value in rabbit_cfg.items(): 49 | print(f" {key}: '{value}'") 50 | 51 | # Aplicar la nueva lógica 52 | print("\n🎯 APLICANDO NUEVA LÓGICA (config.ini tiene prioridad):") 53 | print("-" * 50) 54 | 55 | def get_config_or_message_value(config_key, message_key, default_value=""): 56 | """Lógica exacta implementada en process_handler.py""" 57 | config_value = config.get("RabbitMq", config_key, fallback="") if config.has_section('RabbitMq') else "" 58 | if config_value and str(config_value).strip(): 59 | print(f" {config_key}: 📄 config.ini -> '{config_value}'") 60 | return config_value 61 | else: 62 | message_value = rabbit_cfg.get(message_key, default_value) 63 | print(f" {config_key}: 📡 SocketIO -> '{message_value}' (config vacío)") 64 | return message_value 65 | 66 | # Aplicar la lógica 67 | host = get_config_or_message_value("host", "host") 68 | port_str = get_config_or_message_value("port", "port", "5672") 69 | user = get_config_or_message_value("user", "user", "guest") 70 | pwd = get_config_or_message_value("password", "password", "guest") 71 | vhost = get_config_or_message_value("vhost", "vhost", "/") 72 | queue = get_config_or_message_value("queue", "queue", "") 73 | 74 | # Convertir puerto a entero 75 | try: 76 | port = int(port_str) 77 | except (ValueError, TypeError): 78 | print(f" ⚠️ Puerto inválido '{port_str}', usando 5672 por defecto") 79 | port = 5672 80 | 81 | print("\n🎯 CONFIGURACIÓN FINAL QUE USARÍA LA APLICACIÓN:") 82 | print("=" * 50) 83 | print(f" Host: {host}") 84 | print(f" Port: {port}") 85 | print(f" User: {user}") 86 | print(f" Password: {'*' * len(pwd) if pwd else '(vacío)'}") 87 | print(f" VHost: {vhost}") 88 | print(f" Queue: {queue}") 89 | 90 | print("\n✅ VENTAJAS DE LA NUEVA LÓGICA:") 91 | print(" • Config.ini siempre tiene prioridad (control local)") 92 | print(" • SocketIO solo completa campos vacíos en config.ini") 93 | print(" • Logs claros muestran el origen de cada valor") 94 | print(" • Facilita debugging y configuración manual") 95 | 96 | print("\n💡 PARA PROBAR:") 97 | print(" 1. Modifica valores en ~/.config/Fiscalberry/config.ini") 98 | print(" 2. Esos valores tendrán prioridad sobre cualquier mensaje SocketIO") 99 | print(" 3. Deja campos vacíos en config.ini para usar valores remotos") 100 | 101 | if __name__ == "__main__": 102 | demonstrate_priority_logic() 103 | -------------------------------------------------------------------------------- /docs/logica_prioridad_rabbitmq.md: -------------------------------------------------------------------------------- 1 | # Lógica de Prioridad RabbitMQ: Config.ini vs SocketIO 2 | 3 | ## Resumen de la Implementación 4 | 5 | Hemos implementado una lógica de prioridad robusta en el archivo `process_handler.py` que garantiza que **cada parámetro** de configuración de RabbitMQ sigue la misma regla de prioridad: 6 | 7 | ### 🎯 Regla de Prioridad (por parámetro individual) 8 | 9 | Para cada campo (`host`, `port`, `user`, `password`, `vhost`, `queue`): 10 | 11 | 1. **Primera prioridad**: Valor en `config.ini` (si existe y no está vacío) 12 | 2. **Segunda prioridad**: Valor del mensaje SocketIO 13 | 3. **Tercera prioridad**: Valor por defecto 14 | 15 | ### 📄 Código Implementado 16 | 17 | ```python 18 | def get_config_or_message_value(config_key, message_key, default_value=""): 19 | """ 20 | Obtiene valor del config.ini si existe y no está vacío, sino del mensaje SocketIO. 21 | Prioridad: config.ini -> mensaje SocketIO -> valor por defecto 22 | """ 23 | config_value = self.config.get("RabbitMq", config_key, fallback="") 24 | if config_value and str(config_value).strip(): 25 | logger.info(f"{config_key}: usando config.ini -> {config_value}") 26 | return config_value 27 | else: 28 | message_value = rabbit_cfg.get(message_key, default_value) 29 | logger.info(f"{config_key}: config.ini vacío, usando SocketIO -> {message_value}") 30 | return message_value 31 | 32 | # Aplicar la lógica a todos los parámetros 33 | host = get_config_or_message_value("host", "host") 34 | port_str = get_config_or_message_value("port", "port", "5672") 35 | user = get_config_or_message_value("user", "user", "guest") 36 | pwd = get_config_or_message_value("password", "password", "guest") 37 | vhost = get_config_or_message_value("vhost", "vhost", "/") 38 | queue = get_config_or_message_value("queue", "queue", "") 39 | ``` 40 | 41 | ### ✅ Ventajas de la Nueva Lógica 42 | 43 | 1. **Control Local Total**: Los administradores pueden forzar cualquier valor específico en `config.ini` 44 | 2. **Fallback Inteligente**: Campos vacíos se completan automáticamente desde SocketIO 45 | 3. **Logs Transparentes**: Cada valor muestra claramente su origen 46 | 4. **Configuración Granular**: Cada parámetro puede tener diferente origen sin afectar a los demás 47 | 5. **Backwards Compatible**: Funciona con configuraciones existentes 48 | 49 | ### 🧪 Casos de Prueba Verificados 50 | 51 | #### Caso 1: Config.ini completo 52 | ```ini 53 | [RabbitMq] 54 | host = localhost 55 | port = 5672 56 | user = admin 57 | password = secret 58 | vhost = /prod 59 | queue = my_queue 60 | ``` 61 | **Resultado**: Se usan TODOS los valores del `config.ini`, ignorando SocketIO. 62 | 63 | #### Caso 2: Config.ini parcial 64 | ```ini 65 | [RabbitMq] 66 | host = localhost 67 | port = 68 | user = admin 69 | password = 70 | vhost = /prod 71 | queue = 72 | ``` 73 | **Resultado**: 74 | - `host`, `user`, `vhost` → del `config.ini` 75 | - `port`, `password`, `queue` → del mensaje SocketIO 76 | 77 | #### Caso 3: Config.ini vacío 78 | ```ini 79 | [RabbitMq] 80 | host = 81 | port = 82 | user = 83 | password = 84 | vhost = 85 | queue = 86 | ``` 87 | **Resultado**: Se usan TODOS los valores del mensaje SocketIO. 88 | 89 | ### 🔧 Herramientas de Diagnóstico 90 | 91 | Se han creado scripts de prueba para verificar la lógica: 92 | 93 | 1. **`test_config_priority.py`**: Pruebas unitarias automatizadas 94 | 2. **`demo_priority_logic.py`**: Demostración interactiva con el `config.ini` real 95 | 3. **`rabbitmq_check.py`**: Diagnóstico completo de conectividad RabbitMQ 96 | 97 | ### 💡 Uso Práctico 98 | 99 | **Para forzar un valor específico:** 100 | ```bash 101 | # Editar config.ini 102 | vim ~/.config/Fiscalberry/config.ini 103 | 104 | # Establecer el valor deseado 105 | [RabbitMq] 106 | host = mi-servidor-rabbitmq.com 107 | ``` 108 | 109 | **Para permitir configuración remota:** 110 | ```bash 111 | # Dejar campo vacío en config.ini 112 | [RabbitMq] 113 | host = 114 | # Este valor vendrá del mensaje SocketIO 115 | ``` 116 | 117 | ### 🚀 Mejoras Incluidas 118 | 119 | 1. **Manejo de Errores Mejorado**: DNS, autenticación, red 120 | 2. **Backoff Exponencial**: Reintentos inteligentes 121 | 3. **Logging Detallado**: Origen claro de cada configuración 122 | 4. **Validación de Tipos**: Puerto convertido a entero con fallback 123 | 5. **Diagnósticos Automáticos**: Scripts para troubleshooting 124 | 125 | Esta implementación proporciona un control granular y transparente sobre la configuración de RabbitMQ, facilitando tanto la administración manual como la configuración automática remota. 126 | -------------------------------------------------------------------------------- /docs/rabbitmq_error_handling.md: -------------------------------------------------------------------------------- 1 | # Mejoras en el manejo de errores de RabbitMQ 2 | 3 | ## Problemas identificados 4 | 5 | El error original mostraba: 6 | ``` 7 | ERROR:pika.adapters.utils.selector_ioloop_adapter:Address resolution failed: gaierror(-2, 'Name or service not known') 8 | socket.gaierror: [Errno -2] Name or service not known 9 | ``` 10 | 11 | Este error indicaba que el hostname 'rabbitmq' no se podía resolver, pero el manejo de errores era genérico y no proporcionaba información útil al usuario. 12 | 13 | ## Mejoras implementadas 14 | 15 | ### 1. Manejo específico de errores en `process_handler.py` 16 | 17 | - **Errores DNS**: Se detectan específicamente errores `socket.gaierror` y se proporcionan sugerencias claras 18 | - **Errores de autenticación**: Se capturan errores de `ProbableAuthenticationError` y `ProbableAccessDeniedError` 19 | - **Errores de conectividad**: Se distinguen errores de red vs errores de RabbitMQ 20 | - **Backoff exponencial**: Se implementa un sistema de reintentos inteligente que evita saturar el servidor 21 | 22 | ### 2. Verificación previa de conectividad 23 | 24 | Se añadió `_check_network_connectivity()` que: 25 | - Verifica resolución DNS antes de intentar conectar 26 | - Prueba la conectividad del puerto TCP 27 | - Falla rápido para evitar timeouts largos 28 | 29 | ### 3. Configuración mejorada de timeouts en `consumer.py` 30 | 31 | - `socket_timeout=10`: Timeout más corto para detectar errores de red rápidamente 32 | - `connection_attempts=1`: Solo un intento, el retry lo maneja process_handler 33 | - `retry_delay=1`: Delay corto entre intentos 34 | 35 | ### 4. Herramienta de diagnóstico 36 | 37 | Se creó `rabbitmq_check.py` para: 38 | - Diagnosticar problemas de DNS, red y RabbitMQ por separado 39 | - Proporcionar sugerencias específicas para cada tipo de error 40 | - Permitir pruebas rápidas sin tener que ejecutar toda la aplicación 41 | 42 | ## Uso de la herramienta de diagnóstico 43 | 44 | ```bash 45 | # Diagnóstico básico 46 | python src/fiscalberry/diagnostics/rabbitmq_check.py 47 | 48 | # Usar configuración específica 49 | python src/fiscalberry/diagnostics/rabbitmq_check.py --host localhost --port 5672 50 | 51 | # Usar configuración de Fiscalberry 52 | python src/fiscalberry/diagnostics/rabbitmq_check.py --from-config 53 | ``` 54 | 55 | ## Comportamiento del nuevo sistema de reintentos 56 | 57 | 1. **Primeros 3 intentos**: Reintento cada 5 segundos 58 | 2. **Después del intento 3**: Backoff exponencial (10s, 20s, 40s, etc.) 59 | 3. **Máximo delay**: 300 segundos (5 minutos) 60 | 4. **Verificación de conectividad**: Se verifica DNS y puerto antes de cada intento RabbitMQ 61 | 62 | ## Mensajes de error mejorados 63 | 64 | En lugar de: 65 | ``` 66 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error en RabbitMQConsumer: [Errno -2] Name or service not known 67 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s... 68 | ``` 69 | 70 | Ahora se muestra: 71 | ``` 72 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error de resolución DNS para 'rabbitmq': [Errno -2] Name or service not known 73 | ERROR:fiscalberry.common.rabbitmq.process_handler:Posibles soluciones: 74 | ERROR:fiscalberry.common.rabbitmq.process_handler:1. Verificar que el hostname 'rabbitmq' esté configurado correctamente 75 | ERROR:fiscalberry.common.rabbitmq.process_handler:2. Verificar conectividad de red 76 | ERROR:fiscalberry.common.rabbitmq.process_handler:3. Verificar que el servidor RabbitMQ esté ejecutándose 77 | ERROR:fiscalberry.common.rabbitmq.process_handler:4. Considerar usar una IP directa en lugar del hostname 78 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 1/3 - esperando 5s antes del siguiente intento... 79 | ``` 80 | 81 | ## Soluciones comunes 82 | 83 | ### Error DNS (Name or service not known) 84 | 85 | 1. **Configurar /etc/hosts**: 86 | ``` 87 | 127.0.0.1 rabbitmq 88 | ``` 89 | 90 | 2. **Usar IP directa en configuración**: 91 | ```ini 92 | [RabbitMq] 93 | host = 127.0.0.1 94 | ``` 95 | 96 | 3. **Verificar que RabbitMQ esté ejecutándose en Docker**: 97 | ```bash 98 | docker ps | grep rabbitmq 99 | ``` 100 | 101 | ### Error de conexión (Connection refused) 102 | 103 | 1. Verificar que RabbitMQ esté ejecutándose 104 | 2. Verificar firewall 105 | 3. Verificar puerto correcto (por defecto 5672) 106 | 107 | ### Error de autenticación 108 | 109 | 1. Verificar usuario y contraseña 110 | 2. Verificar permisos en el vhost 111 | 3. Crear usuario si no existe: 112 | ```bash 113 | rabbitmqctl add_user myuser mypassword 114 | rabbitmqctl set_permissions -p / myuser ".*" ".*" ".*" 115 | ``` 116 | -------------------------------------------------------------------------------- /docs/mejoras_rabbitmq_resumen.md: -------------------------------------------------------------------------------- 1 | # Resumen de mejoras en el manejo de errores de RabbitMQ 2 | 3 | ## Archivos modificados 4 | 5 | ### 1. `src/fiscalberry/common/rabbitmq/process_handler.py` 6 | **Mejoras principales:** 7 | - ✅ Manejo específico de errores DNS (`socket.gaierror`) 8 | - ✅ Manejo específico de errores de autenticación y permisos 9 | - ✅ Sistema de backoff exponencial para reintentos 10 | - ✅ Verificación previa de conectividad de red 11 | - ✅ Mensajes de error descriptivos con sugerencias 12 | - ✅ Importación de módulos necesarios (`socket`, `pika.exceptions`) 13 | 14 | ### 2. `src/fiscalberry/common/rabbitmq/consumer.py` 15 | **Mejoras principales:** 16 | - ✅ Timeouts más cortos para detección rápida de errores 17 | - ✅ Configuración optimizada para fallar rápido 18 | - ✅ Reducción de intentos de reconexión automática 19 | 20 | ### 3. `src/fiscalberry/diagnostics/rabbitmq_check.py` (NUEVO) 21 | **Funcionalidades:** 22 | - ✅ Diagnóstico paso a paso (DNS → Red → RabbitMQ) 23 | - ✅ Sugerencias específicas para cada tipo de error 24 | - ✅ Integración con configuración de Fiscalberry 25 | - ✅ Herramienta CLI independiente 26 | 27 | ### 4. `src/fiscalberry/diagnostics/test_error_handling.py` (NUEVO) 28 | **Funcionalidades:** 29 | - ✅ Script de prueba para verificar mejoras 30 | - ✅ Simulación de errores DNS 31 | - ✅ Verificación del sistema de backoff 32 | 33 | ### 5. `docs/rabbitmq_error_handling.md` (NUEVO) 34 | **Contenido:** 35 | - ✅ Documentación completa de las mejoras 36 | - ✅ Guía de solución de problemas comunes 37 | - ✅ Ejemplos de uso de herramientas 38 | 39 | ## Comportamiento antes vs después 40 | 41 | ### ANTES (comportamiento original): 42 | ``` 43 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error en RabbitMQConsumer: [Errno -2] Name or service not known 44 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s... 45 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s... 46 | WARNING:fiscalberry.common.rabbitmq.process_handler:RabbitMQConsumer detenido; reintentando en 5 s... 47 | ... 48 | ``` 49 | - ❌ Error genérico sin contexto 50 | - ❌ Reintentos cada 5 segundos indefinidamente 51 | - ❌ Sin información sobre posibles soluciones 52 | - ❌ Sin diferenciación entre tipos de error 53 | 54 | ### DESPUÉS (comportamiento mejorado): 55 | ``` 56 | ERROR:fiscalberry.common.rabbitmq.process_handler:Error de resolución DNS para 'rabbitmq': [Errno -2] Name or service not known 57 | ERROR:fiscalberry.common.rabbitmq.process_handler:Posibles soluciones: 58 | ERROR:fiscalberry.common.rabbitmq.process_handler:1. Verificar que el hostname 'rabbitmq' esté configurado correctamente 59 | ERROR:fiscalberry.common.rabbitmq.process_handler:2. Verificar conectividad de red 60 | ERROR:fiscalberry.common.rabbitmq.process_handler:3. Verificar que el servidor RabbitMQ esté ejecutándose 61 | ERROR:fiscalberry.common.rabbitmq.process_handler:4. Considerar usar una IP directa en lugar del hostname 62 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 1/3 - esperando 5s antes del siguiente intento... 63 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 2/3 - esperando 5s antes del siguiente intento... 64 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 3/3 - esperando 5s antes del siguiente intento... 65 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 4 con backoff exponencial - esperando 10s antes del siguiente intento... 66 | WARNING:fiscalberry.common.rabbitmq.process_handler:Reintento 5 con backoff exponencial - esperando 20s antes del siguiente intento... 67 | ... 68 | ``` 69 | - ✅ Error específico con identificación clara del problema 70 | - ✅ Sugerencias concretas para solucionar el error 71 | - ✅ Sistema de backoff exponencial inteligente 72 | - ✅ Diferenciación entre tipos de error (DNS, red, autenticación, etc.) 73 | 74 | ## Herramientas de diagnóstico 75 | 76 | ### Uso básico: 77 | ```bash 78 | python src/fiscalberry/diagnostics/rabbitmq_check.py 79 | ``` 80 | 81 | ### Con configuración específica: 82 | ```bash 83 | python src/fiscalberry/diagnostics/rabbitmq_check.py --host localhost --port 5672 --user myuser --password mypass 84 | ``` 85 | 86 | ### Usando configuración de Fiscalberry: 87 | ```bash 88 | python src/fiscalberry/diagnostics/rabbitmq_check.py --from-config 89 | ``` 90 | 91 | ## Beneficios principales 92 | 93 | 1. **Diagnóstico más rápido**: Los errores se identifican y categorizan inmediatamente 94 | 2. **Menor carga en el sistema**: Backoff exponencial evita spam de reintentos 95 | 3. **Mejor experiencia del usuario**: Mensajes claros con sugerencias específicas 96 | 4. **Herramientas de depuración**: Scripts independientes para diagnosticar problemas 97 | 5. **Documentación completa**: Guías paso a paso para solucionar problemas 98 | 99 | ## Casos de uso cubiertos 100 | 101 | - ✅ Error DNS (hostname no encontrado) 102 | - ✅ Error de conectividad de red (puerto cerrado/firewall) 103 | - ✅ Error de autenticación (usuario/contraseña incorrectos) 104 | - ✅ Error de permisos (acceso denegado al vhost) 105 | - ✅ Error de configuración de RabbitMQ 106 | - ✅ Timeouts de conexión 107 | - ✅ Servidor RabbitMQ no disponible 108 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/old_fiscalberry_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from kivy.app import App 3 | from kivy.uix.label import Label 4 | from kivy.uix.scrollview import ScrollView 5 | from kivy.uix.boxlayout import BoxLayout 6 | from kivy.uix.image import Image 7 | from kivy.clock import Clock 8 | from kivy.properties import StringProperty, BooleanProperty 9 | from jnius import autoclass 10 | from fiscalberry.common.Configberry import Configberry 11 | from twisted.internet.protocol import Protocol, ClientFactory 12 | from twisted.internet import reactor 13 | 14 | # Importo el módulo que se encarga de la comunicación con el servidor 15 | from fiscalberry.common.fiscalberry_logger import getLogger 16 | 17 | logger = getLogger() 18 | 19 | SERVICE_NAME = u'{packagename}.Service{servicename}'.format( 20 | packagename=u'com.paxapos.fiscalberry', 21 | servicename=u'Fiscalberryservice' 22 | ) 23 | 24 | 25 | configberry = Configberry() 26 | sio = None 27 | 28 | class LogWidget(ScrollView): 29 | 30 | logs = [] 31 | 32 | 33 | def __init__(self, **kwargs): 34 | super(LogWidget, self).__init__(**kwargs) 35 | 36 | self.background_color = (1, 0.75, 0.8, 1) # Set pink background color 37 | 38 | def add_log(self, log): 39 | self.logs.append(log) 40 | self.update_logs() 41 | 42 | def update_logs(self): 43 | self.clear_widgets() 44 | for log in self.logs: 45 | self.add_widget(Label(text=log, size_hint_y=None, height=40)) 46 | 47 | 48 | class ConnectedImage(Image): 49 | source = "assets/disconnected.png" 50 | 51 | 52 | 53 | class MainLayout(BoxLayout): 54 | 55 | def __init__(self, **kwargs): 56 | super(MainLayout, self).__init__(**kwargs) 57 | self.log_widget = LogWidget(size_hint=(1, 0.5)) 58 | self.add_widget(self.log_widget) 59 | 60 | 61 | class FiscalberryApp(App): 62 | assetpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 63 | 64 | background_image = StringProperty(f"{assetpath}/assets/bg.jpg") 65 | logo_image = StringProperty(f"{assetpath}/assets/fiscalberry.png") 66 | disconnected_image = StringProperty(f"{assetpath}/assets/disconnected.png") 67 | connected_image = StringProperty(f"{assetpath}/assets/connected.png") 68 | 69 | connected: bool = BooleanProperty(False) 70 | 71 | fiscalberry_sio = None 72 | 73 | sio = None 74 | 75 | def __init__(self, **kwargs): 76 | super(FiscalberryApp, self).__init__(**kwargs) 77 | 78 | # load APP config 79 | serverUrl = configberry.config.get("SERVIDOR", "sio_host", fallback="") 80 | self.sioServerUrl = serverUrl if serverUrl else self.sioServerUrl 81 | 82 | # Crear la fábrica del cliente y conectar al socket 83 | factory = AppInterConFactory(self) 84 | reactor.connectUNIX("/data/data/com.paxapos.fiscalberry/files/fiscalberry_socket", factory) 85 | 86 | def start_service(self, finishing=False): 87 | service = autoclass(SERVICE_NAME) 88 | mActivity = autoclass(u'org.kivy.android.PythonActivity').mActivity 89 | argument = '' 90 | if finishing: 91 | argument = '{"stop_service": 1}' 92 | service.start(mActivity, argument) 93 | if finishing: 94 | self.service = None 95 | else: 96 | self.service = service 97 | 98 | self.connected = True 99 | 100 | def stop_service(self): 101 | service = autoclass(SERVICE_NAME) 102 | mActivity = autoclass('org.kivy.android.PythonActivity').mActivity 103 | service.stop(mActivity) 104 | self.start_service(finishing=True) 105 | self.connected = False 106 | 107 | def start_fiscaberry_service(): 108 | service = autoclass('com.paxapos.fiscalberry.ServiceFiscalberryservice') 109 | mActivity = autoclass('org.kivy.android.PythonActivity').mActivity 110 | argument = '' 111 | service.start(mActivity, argument) 112 | 113 | def build(self): 114 | self.root = MainLayout() 115 | self.log_widget = self.root.log_widget 116 | return self.root 117 | 118 | def add_log(self, text): 119 | self.log_widget.add_log(text) 120 | 121 | 122 | def store_new_host(self, value): 123 | host = value 124 | logger.info(f"FiscalberryApp: vino para guardar sio_host:: {host} antes estaba {self.sioServerUrl}") 125 | if host != self.sioServerUrl: 126 | logger.info(f"FiscalberryApp: se guarda nuevo sio_host:: {host}") 127 | configberry.writeKeyForSection("SERVIDOR", "sio_host", host) 128 | 129 | 130 | def on_resume(self): 131 | if self.service: 132 | self.start_service() 133 | 134 | def on_start(self): 135 | """ 136 | This function is executed when the application starts. 137 | It is a Kivy function. 138 | """ 139 | 140 | # Iniciar con el ícono rojo por defecto 141 | self.start_service() 142 | logger.info("FiscalberryApp: Iniciando la aplicación") 143 | 144 | 145 | def on_stop(self): 146 | """ 147 | This function is executed when the application is closed. 148 | It is a Kivy function. 149 | """ 150 | self.stop_service() 151 | logger.info("FiscalberryApp: Deteniendo la aplicación") 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/fiscalberryservice/android.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import os 3 | from jnius import autoclass 4 | 5 | # -*- coding: utf-8 -*- 6 | 7 | """ 8 | Basic structure for an Android background service using Python. 9 | This typically requires a framework like Kivy (with python-for-android) 10 | or Chaquopy to bridge Python with the Android OS. 11 | """ 12 | 13 | 14 | # Using jnius (common with Kivy/python-for-android) to interact with Android APIs 15 | try: 16 | # Example: Get access to the service context if running under PythonService 17 | # PythonService = autoclass('org.kivy.android.PythonService') 18 | # service = PythonService.mService 19 | pass 20 | except ImportError: 21 | print("jnius not found. Android-specific features may not work.") 22 | autoclass = None 23 | service = None 24 | 25 | def run_service_logic(): 26 | """Contains the main logic for the background service.""" 27 | print("Android Service: Starting background logic.") 28 | 29 | # Example: Retrieve an argument passed when starting the service 30 | # This environment variable is often set by the service starter (e.g., Kivy's PythonService) 31 | service_argument = os.environ.get('PYTHON_SERVICE_ARGUMENT', 'No argument provided') 32 | print(f"Android Service: Received argument: {service_argument}") 33 | 34 | # Main service loop 35 | counter = 0 36 | while True: 37 | print(f"Android Service: Running... Loop count: {counter}") 38 | 39 | # --- Add your background tasks here --- 40 | # Examples: 41 | # - Polling a server for updates 42 | # - Processing data in the background 43 | # - Interacting with hardware (requires specific permissions and APIs) 44 | # - Sending notifications (requires Android API calls via jnius/plyer) 45 | 46 | # Example: Show a Toast notification (requires context and jnius) 47 | # if autoclass and service: 48 | # try: 49 | # Toast = autoclass('android.widget.Toast') 50 | # CharSequence = autoclass('java.lang.CharSequence') 51 | # context = service.getApplicationContext() 52 | # if context: 53 | # def show_toast(): 54 | # text = CharSequence(f"Service running {counter}") 55 | # toast = Toast.makeText(context, text, Toast.LENGTH_SHORT) 56 | # toast.show() 57 | # # Toasts must be shown on the UI thread 58 | # PythonActivity = autoclass('org.kivy.android.PythonActivity') 59 | # PythonActivity.mActivity.runOnUiThread(show_toast) 60 | # except Exception as e: 61 | # print(f"Android Service: Error showing toast - {e}") 62 | 63 | # Sleep for a period before the next iteration 64 | sleep(15) # Sleep for 15 seconds 65 | counter += 1 66 | 67 | # Add conditions to stop the service if necessary, 68 | # although services are often designed to run until explicitly stopped. 69 | # For example: 70 | # if should_stop_service(): 71 | # print("Android Service: Stopping condition met.") 72 | # break 73 | 74 | print("Android Service: Background logic finished.") 75 | 76 | if __name__ == "__main__": 77 | # This block is executed when the script is run directly. 78 | # When launched as an Android service by the framework (e.g., Kivy), 79 | # the framework typically imports and runs this script, potentially 80 | # calling a specific function or just executing the module level code. 81 | # The `run_service_logic()` function contains the core tasks. 82 | run_service_logic() 83 | 84 | # --- Notes on Deployment as an Android Service --- 85 | # 86 | # 1. **Framework:** Use Kivy (with buildozer) or Chaquopy. 87 | # 2. **Configuration (Buildozer Example):** 88 | # In `buildozer.spec`, define the service: 89 | # ``` 90 | # services = YourServiceName:./src/fiscalberryservice/android.py 91 | # ``` 92 | # Replace `YourServiceName` and adjust the path. 93 | # 3. **Starting the Service:** 94 | # Start it from your main app (e.g., Kivy `main.py`) using Android Intents via jnius: 95 | # ```python 96 | # from jnius import autoclass 97 | # import os 98 | # 99 | # PythonActivity = autoclass('org.kivy.android.PythonActivity') 100 | # activity = PythonActivity.mActivity 101 | # Intent = autoclass('android.content.Intent') 102 | # 103 | # service_name = 'YourServiceName' # Must match buildozer.spec 104 | # service_class_name = f'{activity.getPackageName()}.Service{service_name}' 105 | # 106 | # intent = Intent() 107 | # intent.setClassName(activity, service_class_name) 108 | # 109 | # # Pass data to the service (optional) 110 | # argument = "Data for the service" 111 | # intent.putExtra('PYTHON_SERVICE_ARGUMENT', argument) 112 | # os.environ['PYTHON_SERVICE_ARGUMENT'] = argument # Set env var too 113 | # 114 | # activity.startService(intent) 115 | # ``` 116 | # 4. **Permissions:** Add necessary permissions to `buildozer.spec`: 117 | # ``` 118 | # android.permissions = INTERNET, FOREGROUND_SERVICE, ... 119 | # ``` 120 | # `FOREGROUND_SERVICE` is often required for long-running tasks on newer Android versions. 121 | # 5. **Foreground Service:** For long-running tasks, consider running as a foreground service 122 | # to prevent Android from killing it. This requires showing a persistent notification. 123 | # You'll need more jnius code to create the notification channel and notification itself. -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | 2 | from fiscalberry.common.ComandosHandler import ComandosHandler 3 | import sys 4 | from fiscalberry.common.printer_detector import listar_impresoras 5 | 6 | ''' 7 | si ejecuto el script sin argumentos que me de un listado de opciones y ayuda de uso 8 | estre ecript se ejecuta con 2 argumentos, el primero es es el tipo de comando a enviar y 9 | las opciones son 10 | - remito1 11 | - remito2 12 | - facturaA 13 | - facturaB 14 | - cierreCaja 15 | - comanda 16 | 17 | y el segundo parametro es el nombre de la impresora a la que se le envia el comando, es opcional 18 | si no se especifica se envia a la impresora Dummy 19 | ''' 20 | 21 | 22 | 23 | # Ejemplo de uso 24 | impresoras = listar_impresoras() 25 | print("Impresoras instaladas:", impresoras) 26 | 27 | class ComandosEnum: 28 | REMITO1 = "remito1" 29 | REMITO2 = "remito2" 30 | FACTURAA = "facturaA" 31 | FACTURAB = "facturaB" 32 | CIERRECAJA = "cierreCaja" 33 | COMANDA1 = "comanda1" 34 | 35 | 36 | 37 | if len(sys.argv) == 1: 38 | print("Listado de opciones y ayuda de uso:") 39 | for i, comando in enumerate(ComandosEnum.__dict__.keys()): 40 | if not comando.startswith("__"): 41 | print(f"{i+1}. {comando}") 42 | sys.exit() 43 | 44 | comando = sys.argv[1] 45 | 46 | #si no se especifica la comandera, se envia a la comandera Dummy, si se especifica 47 | #se envia a la comandera especificada 48 | if len(sys.argv) > 2: 49 | comandera = sys.argv[2] 50 | else: 51 | comandera = 'Dummy' 52 | 53 | 54 | 55 | comandosDisponibles = {} 56 | comandosDisponibles[ComandosEnum.REMITO1] = {'printRemito': { 57 | 'encabezado': {'imprimir_fecha_remito': ''}, 58 | 'items': [ 59 | {'alic_iva': '21.00', 'qty': 6, 'ds': "None", 'importe': 50, 'order': '0'}, 60 | ], 61 | 'setTrailer': [ 62 | 'Mozo: Ludmila', 'Mesa: 33' 63 | ]}, 64 | 'printerName': 'File' 65 | } 66 | 67 | comandosDisponibles[ComandosEnum.REMITO2] = {'printRemito': {'encabezado': {'imprimir_fecha_remito': ''}, 'items': [ 68 | {'alic_iva': '21.00', 'qty': '1', 'ds': 'Producto', 'importe': 1, 'order': None}, 69 | {'alic_iva': '21.00', 'qty': '1', 'ds': None, 'importe': '2200', 'order': None}], 70 | 'setTrailer': ['Mozo: Cena Show', 'Mesa: prueba']}, 'printerName': 'dummy'} 71 | 72 | comandosDisponibles[ComandosEnum.FACTURAB] = {'printFacturaElectronica': {'encabezado': {'nombre_comercio': 'Paxapoga', 'razon_social': 'Riotorno SRL', 'cuit_empresa': '30715582593', 'domicilio_comercial': 'Beruti 4643', 'tipo_responsable': 'Resp. Inscripto', 'inicio_actividades': '', 'tipo_comprobante': '"B"', 'tipo_comprobante_codigo': '006', 'numero_comprobante': '0010-00011795', 'fecha_comprobante': '2024-08-29', 'documento_cliente': '0', 'nombre_cliente': '', 'domicilio_cliente': '', 'nombre_tipo_documento': 'Sin identificar', 'cae': '74353157058451', 'cae_vto': '2024-09-08', 'importe_total': '1.00', 'importe_neto': '4876.86', 'importe_iva': '1024.14', 'moneda': 'PES', 'ctz': 1, 'tipoDocRec': 99, 'tipoCodAut': 'E'}, 'items': [{'alic_iva': 0.21, 'importe': 1, 'ds': 'Producto', 'qty': 1}], 'pagos': [], 'setTrailer': ['Mozo: Cena Show', 'Mesa: prueba']}, 'printerName': comandera} 73 | 74 | comandosDisponibles[ComandosEnum.FACTURAA] = {"printFacturaElectronica": {"encabezado": {"nombre_comercio": "Riotorno SRL", "razon_social": "Riotorno SRL", "cuit_empresa": "30715582593", "domicilio_comercial": "una calle 2232", "tipo_responsable": "Resp. Inscripto", "inicio_actividades": None, "tipo_comprobante": '"A"', "tipo_comprobante_codigo": "001", "numero_comprobante": "0010-00000016", "fecha_comprobante": "2024-09-02", "documento_cliente": "33709585229", "nombre_cliente": "Google Arg. SRL", "domicilio_cliente": "", "nombre_tipo_documento": "CUIT", "cae": "74363194522383", "cae_vto": "2024-09-12", "importe_total": "2000.00", "importe_neto": "1652.88", "importe_iva": "347.12", "moneda": "PES", "ctz": 1, "tipoDocRec": 99, "tipoCodAut": "E"}, "items": [{"alic_iva": 164.88, "importe": 950, "ds": "DAIQUIRI S/A MARACUYA", "qty": 1}, {"alic_iva": 164.88, "importe": 950, "ds": "DAIQUIRI S/A FRUTILLA", "qty": 1}, {"alic_iva": 8.68, "importe": 50, "ds": "producto en otra impresora", "qty": 2}], "pagos": [], "setTrailer": ["Mozo: Riotorno SRL", "Mesa: 2"]}, "printerName": comandera} 75 | 76 | comandosDisponibles[ComandosEnum.COMANDA1] = {'printComanda': {'comanda': {'id': '629', 'created': '2024-09-02 08:48:20', 'observacion': 'observacion general', 'entradas': [{'nombre': 'cerveza con variante', 'cant': '1', 'sabores': ['(1) IMPERIAL RUBIA TIRADA ', '(3) Imperial Rubia Tirada x Lt']}], 'platos': [{'nombre': 'OLD FASHIONED', 'cant': '2'}, {'nombre': 'BOULEVARDIER', 'cant': '1'}, {'nombre': 'WHITE RUSSIAN', 'cant': '1'}, {'nombre': 'PISCO SOUR', 'cant': '1'}, {'nombre': 'MOJITO DE FRUTOS ROJOS', 'cant': '1'}, {'nombre': 'DAIQUIRI S/A FRUTILLA', 'cant': '1'}, {'nombre': 'DAIQUIRI S/A MARACUYA', 'cant': '1', 'observacion': 'un detalle del producto'}, {'nombre': 'producto en otra impresora', 'cant': '2'}]}, 'setTrailer': [' ', 'Mozo: Riotorno SRL', 'Mesa: 112', ' ']}, 'printerName': comandera} 77 | 78 | 79 | 80 | comandoHandler = ComandosHandler() 81 | 82 | 83 | # si comando tiene el nombre all, ejecutar todos 84 | if comando == "all": 85 | for comando in comandosDisponibles: 86 | comandoHandler.send_command(comandosDisponibles.get(comando)) 87 | sys.exit() 88 | 89 | 90 | # si comando existe en el listado de comandos disponibles, envio el comando a 91 | # la impresora 92 | if comando in comandosDisponibles: 93 | comandoHandler.send_command(comandosDisponibles.get(comando)) 94 | else: 95 | print(f"El comando {comando} no existe en la lista de comandos disponibles") 96 | sys.exit() 97 | 98 | -------------------------------------------------------------------------------- /test_config_priority.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script de prueba para verificar la lógica de prioridad config.ini vs SocketIO 4 | en la configuración de RabbitMQ. 5 | """ 6 | 7 | import configparser 8 | import tempfile 9 | import os 10 | import sys 11 | sys.path.insert(0, 'src') 12 | 13 | def simulate_config_priority_logic(): 14 | """Simula la lógica de prioridad sin dependencias externas""" 15 | 16 | # Crear un config temporal para las pruebas 17 | config = configparser.ConfigParser() 18 | config.add_section('RabbitMq') 19 | 20 | # CASO 1: Config.ini tiene valores, mensaje SocketIO también 21 | print("=== CASO 1: Config.ini con valores, SocketIO con valores ===") 22 | config.set('RabbitMq', 'host', 'localhost') 23 | config.set('RabbitMq', 'port', '5672') 24 | config.set('RabbitMq', 'user', 'admin') 25 | config.set('RabbitMq', 'password', 'secret') 26 | config.set('RabbitMq', 'vhost', '/prod') 27 | config.set('RabbitMq', 'queue', 'config_queue') 28 | 29 | # Datos del mensaje SocketIO (estos deberían ser ignorados) 30 | rabbit_cfg = { 31 | 'host': 'rabbitmq.server.com', 32 | 'port': '5673', 33 | 'user': 'socketio_user', 34 | 'password': 'socketio_pass', 35 | 'vhost': '/socketio', 36 | 'queue': 'socketio_queue' 37 | } 38 | 39 | def get_config_or_message_value(config_key, message_key, default_value=""): 40 | """Lógica exacta del código real""" 41 | config_value = config.get("RabbitMq", config_key, fallback="") 42 | if config_value and str(config_value).strip(): 43 | print(f" {config_key}: usando config.ini -> {config_value}") 44 | return config_value 45 | else: 46 | message_value = rabbit_cfg.get(message_key, default_value) 47 | print(f" {config_key}: config.ini vacío, usando SocketIO -> {message_value}") 48 | return message_value 49 | 50 | host = get_config_or_message_value("host", "host") 51 | port_str = get_config_or_message_value("port", "port", "5672") 52 | user = get_config_or_message_value("user", "user", "guest") 53 | pwd = get_config_or_message_value("password", "password", "guest") 54 | vhost = get_config_or_message_value("vhost", "vhost", "/") 55 | queue = get_config_or_message_value("queue", "queue", "") 56 | 57 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}") 58 | assert host == 'localhost', f"Expected localhost, got {host}" 59 | assert port_str == '5672', f"Expected 5672, got {port_str}" 60 | assert user == 'admin', f"Expected admin, got {user}" 61 | assert pwd == 'secret', f"Expected secret, got {pwd}" 62 | assert vhost == '/prod', f"Expected /prod, got {vhost}" 63 | assert queue == 'config_queue', f"Expected config_queue, got {queue}" 64 | print("✓ CASO 1 CORRECTO: Se usaron todos los valores del config.ini\n") 65 | 66 | # CASO 2: Config.ini parcialmente vacío 67 | print("=== CASO 2: Config.ini parcialmente vacío ===") 68 | config.set('RabbitMq', 'host', 'localhost') # Tiene valor 69 | config.set('RabbitMq', 'port', '') # Vacío 70 | config.set('RabbitMq', 'user', 'admin') # Tiene valor 71 | config.set('RabbitMq', 'password', '') # Vacío 72 | config.set('RabbitMq', 'vhost', '/prod') # Tiene valor 73 | config.set('RabbitMq', 'queue', '') # Vacío 74 | 75 | host = get_config_or_message_value("host", "host") 76 | port_str = get_config_or_message_value("port", "port", "5672") 77 | user = get_config_or_message_value("user", "user", "guest") 78 | pwd = get_config_or_message_value("password", "password", "guest") 79 | vhost = get_config_or_message_value("vhost", "vhost", "/") 80 | queue = get_config_or_message_value("queue", "queue", "") 81 | 82 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}") 83 | assert host == 'localhost', f"Expected localhost, got {host}" 84 | assert port_str == '5673', f"Expected 5673 (from SocketIO), got {port_str}" 85 | assert user == 'admin', f"Expected admin, got {user}" 86 | assert pwd == 'socketio_pass', f"Expected socketio_pass (from SocketIO), got {pwd}" 87 | assert vhost == '/prod', f"Expected /prod, got {vhost}" 88 | assert queue == 'socketio_queue', f"Expected socketio_queue (from SocketIO), got {queue}" 89 | print("✓ CASO 2 CORRECTO: Se usó config.ini donde tenía valores, SocketIO donde estaba vacío\n") 90 | 91 | # CASO 3: Config.ini completamente vacío 92 | print("=== CASO 3: Config.ini completamente vacío ===") 93 | config.set('RabbitMq', 'host', '') 94 | config.set('RabbitMq', 'port', '') 95 | config.set('RabbitMq', 'user', '') 96 | config.set('RabbitMq', 'password', '') 97 | config.set('RabbitMq', 'vhost', '') 98 | config.set('RabbitMq', 'queue', '') 99 | 100 | host = get_config_or_message_value("host", "host") 101 | port_str = get_config_or_message_value("port", "port", "5672") 102 | user = get_config_or_message_value("user", "user", "guest") 103 | pwd = get_config_or_message_value("password", "password", "guest") 104 | vhost = get_config_or_message_value("vhost", "vhost", "/") 105 | queue = get_config_or_message_value("queue", "queue", "") 106 | 107 | print(f"Resultado final: host={host}, port={port_str}, user={user}, vhost={vhost}, queue={queue}") 108 | assert host == 'rabbitmq.server.com', f"Expected rabbitmq.server.com, got {host}" 109 | assert port_str == '5673', f"Expected 5673, got {port_str}" 110 | assert user == 'socketio_user', f"Expected socketio_user, got {user}" 111 | assert pwd == 'socketio_pass', f"Expected socketio_pass, got {pwd}" 112 | assert vhost == '/socketio', f"Expected /socketio, got {vhost}" 113 | assert queue == 'socketio_queue', f"Expected socketio_queue, got {queue}" 114 | print("✓ CASO 3 CORRECTO: Se usaron todos los valores del SocketIO\n") 115 | 116 | print("🎉 TODAS LAS PRUEBAS PASARON - La lógica de prioridad funciona correctamente") 117 | 118 | if __name__ == "__main__": 119 | simulate_config_priority_logic() 120 | -------------------------------------------------------------------------------- /src/fiscalberry/diagnostics/rabbitmq_check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilidad para diagnosticar problemas de conexión con RabbitMQ. 4 | Ayuda a identificar si el problema es DNS, conectividad de red, o configuración de RabbitMQ. 5 | """ 6 | 7 | import socket 8 | import pika 9 | import sys 10 | import argparse 11 | from typing import Dict, Any 12 | 13 | def check_dns_resolution(host: str) -> bool: 14 | """Verifica si el hostname se puede resolver.""" 15 | try: 16 | result = socket.getaddrinfo(host, None) 17 | print(f"✓ DNS: {host} se resuelve a {result[0][4][0]}") 18 | return True 19 | except socket.gaierror as e: 20 | print(f"✗ DNS: Error resolviendo {host}: {e}") 21 | print(" Sugerencias:") 22 | print(" - Verificar /etc/hosts") 23 | print(" - Verificar configuración DNS") 24 | print(" - Usar IP directa en lugar del hostname") 25 | return False 26 | 27 | def check_port_connectivity(host: str, port: int, timeout: int = 5) -> bool: 28 | """Verifica si el puerto está abierto.""" 29 | try: 30 | with socket.create_connection((host, port), timeout=timeout): 31 | print(f"✓ Red: Puerto {port} en {host} está abierto") 32 | return True 33 | except socket.timeout: 34 | print(f"✗ Red: Timeout conectando a {host}:{port}") 35 | return False 36 | except ConnectionRefusedError: 37 | print(f"✗ Red: Conexión rechazada en {host}:{port}") 38 | print(" Sugerencias:") 39 | print(" - Verificar que RabbitMQ esté ejecutándose") 40 | print(" - Verificar firewall") 41 | return False 42 | except Exception as e: 43 | print(f"✗ Red: Error conectando a {host}:{port}: {e}") 44 | return False 45 | 46 | def check_rabbitmq_connection(host: str, port: int, user: str, password: str, vhost: str = "/") -> bool: 47 | """Verifica la conexión completa a RabbitMQ.""" 48 | try: 49 | params = pika.ConnectionParameters( 50 | host=host, 51 | port=port, 52 | virtual_host=vhost, 53 | credentials=pika.PlainCredentials(user, password), 54 | socket_timeout=10, 55 | connection_attempts=1 56 | ) 57 | 58 | connection = pika.BlockingConnection(params) 59 | channel = connection.channel() 60 | 61 | # Verificar que podemos declarar un exchange temporal 62 | test_exchange = "test_connection_check" 63 | channel.exchange_declare(exchange=test_exchange, exchange_type='direct', durable=False, auto_delete=True) 64 | channel.exchange_delete(exchange=test_exchange) 65 | 66 | connection.close() 67 | print(f"✓ RabbitMQ: Conexión exitosa a {host}:{port}") 68 | print(f" Usuario: {user}") 69 | print(f" VHost: {vhost}") 70 | return True 71 | 72 | except pika.exceptions.ProbableAuthenticationError as e: 73 | print(f"✗ RabbitMQ: Error de autenticación: {e}") 74 | print(" Sugerencias:") 75 | print(f" - Verificar usuario '{user}' existe") 76 | print(f" - Verificar contraseña") 77 | print(f" - Verificar permisos en vhost '{vhost}'") 78 | return False 79 | except pika.exceptions.ProbableAccessDeniedError as e: 80 | print(f"✗ RabbitMQ: Acceso denegado: {e}") 81 | print(" Sugerencias:") 82 | print(f" - Verificar permisos del usuario '{user}' en vhost '{vhost}'") 83 | return False 84 | except pika.exceptions.AMQPConnectionError as e: 85 | print(f"✗ RabbitMQ: Error de conexión AMQP: {e}") 86 | return False 87 | except Exception as e: 88 | print(f"✗ RabbitMQ: Error inesperado: {e}") 89 | return False 90 | 91 | def get_config_from_file(config_file: str = None) -> Dict[str, Any]: 92 | """Intenta leer la configuración del archivo config.ini de Fiscalberry.""" 93 | try: 94 | from fiscalberry.common.Configberry import Configberry 95 | config = Configberry() 96 | return { 97 | 'host': config.get("RabbitMq", "host"), 98 | 'port': int(config.get("RabbitMq", "port")), 99 | 'user': config.get("RabbitMq", "user"), 100 | 'password': config.get("RabbitMq", "password"), 101 | 'vhost': config.get("RabbitMq", "vhost", "/") 102 | } 103 | except Exception as e: 104 | print(f"No se pudo leer configuración de Fiscalberry: {e}") 105 | return {} 106 | 107 | def main(): 108 | parser = argparse.ArgumentParser(description='Diagnosticar conexión RabbitMQ') 109 | parser.add_argument('--host', default='rabbitmq', help='Hostname de RabbitMQ') 110 | parser.add_argument('--port', type=int, default=5672, help='Puerto de RabbitMQ') 111 | parser.add_argument('--user', default='guest', help='Usuario de RabbitMQ') 112 | parser.add_argument('--password', default='guest', help='Contraseña de RabbitMQ') 113 | parser.add_argument('--vhost', default='/', help='Virtual host de RabbitMQ') 114 | parser.add_argument('--from-config', action='store_true', help='Usar configuración de Fiscalberry') 115 | 116 | args = parser.parse_args() 117 | 118 | print("=== Diagnóstico de conexión RabbitMQ ===\n") 119 | 120 | # Si se especifica --from-config, intentar leer del archivo de configuración 121 | if args.from_config: 122 | config = get_config_from_file() 123 | if config: 124 | args.host = config['host'] 125 | args.port = config['port'] 126 | args.user = config['user'] 127 | args.password = config['password'] 128 | args.vhost = config['vhost'] 129 | print(f"Usando configuración de Fiscalberry:") 130 | print(f" Host: {args.host}") 131 | print(f" Puerto: {args.port}") 132 | print(f" Usuario: {args.user}") 133 | print(f" VHost: {args.vhost}\n") 134 | 135 | print(f"Probando conexión a {args.host}:{args.port}...\n") 136 | 137 | # 1. Verificar resolución DNS 138 | dns_ok = check_dns_resolution(args.host) 139 | 140 | # 2. Verificar conectividad de puerto 141 | port_ok = False 142 | if dns_ok: 143 | port_ok = check_port_connectivity(args.host, args.port) 144 | 145 | # 3. Verificar conexión RabbitMQ completa 146 | rabbitmq_ok = False 147 | if port_ok: 148 | rabbitmq_ok = check_rabbitmq_connection(args.host, args.port, args.user, args.password, args.vhost) 149 | 150 | print("\n=== Resumen ===") 151 | print(f"DNS: {'✓' if dns_ok else '✗'}") 152 | print(f"Red: {'✓' if port_ok else '✗'}") 153 | print(f"RabbitMQ: {'✓' if rabbitmq_ok else '✗'}") 154 | 155 | if rabbitmq_ok: 156 | print("\n🎉 Conexión exitosa! RabbitMQ está funcionando correctamente.") 157 | return 0 158 | else: 159 | print("\n❌ Hay problemas con la conexión. Revisar los errores anteriores.") 160 | return 1 161 | 162 | if __name__ == "__main__": 163 | sys.exit(main()) 164 | -------------------------------------------------------------------------------- /src/fiscalberry/common/fiscalberry_sio.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import socketio 3 | import queue 4 | import os 5 | import time 6 | from fiscalberry.common.ComandosHandler import ComandosHandler, TraductorException 7 | from fiscalberry.common.fiscalberry_logger import getLogger 8 | from fiscalberry.common.Configberry import Configberry 9 | from fiscalberry.common.rabbitmq.process_handler import RabbitMQProcessHandler 10 | 11 | 12 | environment = os.getenv('ENVIRONMENT', 'production') 13 | sioLogger = True if environment == 'development' else False 14 | logger = getLogger() 15 | 16 | class FiscalberrySio: 17 | _instance = None 18 | _lock = threading.Lock() 19 | 20 | rabbitmq_thread = None 21 | 22 | def __new__(cls, *a, **k): 23 | with cls._lock: 24 | if not cls._instance: 25 | cls._instance = super().__new__(cls) 26 | cls._instance._initialized = False 27 | return cls._instance 28 | 29 | def __init__(self, server_url: str, uuid: str, namespaces='/paxaprinter', on_message=None): 30 | if self._initialized: 31 | return 32 | self.server_url = server_url 33 | self.uuid = uuid 34 | self.namespaces = namespaces 35 | self.on_message = on_message 36 | self.sio = socketio.Client( 37 | reconnection=True, 38 | reconnection_attempts=0, 39 | reconnection_delay=2, 40 | reconnection_delay_max=15, 41 | logger=True, 42 | engineio_logger=False, 43 | ) 44 | self.stop_event = threading.Event() 45 | self.thread = None 46 | self.config = Configberry() 47 | self.message_queue = queue.Queue() 48 | self.rabbit_handler = RabbitMQProcessHandler() 49 | self._register_events() 50 | self._initialized = True 51 | 52 | def _register_events(self): 53 | ns = self.namespaces 54 | 55 | @self.sio.event(namespace=ns) 56 | def connect(): 57 | logger.info(f"SIO connect, SID={self.sio.sid}") 58 | 59 | @self.sio.event(namespace=ns) 60 | def connect_error(err): 61 | logger.error(f"SIO connect error: {err}") 62 | 63 | @self.sio.event(namespace=ns) 64 | def disconnect(): 65 | logger.info("SIO disconnect") 66 | 67 | @self.sio.event(namespace=ns) 68 | def error(err): 69 | logger.error(f"SIO error: {err}") 70 | 71 | @self.sio.event(namespace=ns) 72 | def start_sio(): 73 | logger.info("SIO start_sio") 74 | 75 | @self.sio.event(namespace=ns) 76 | def adopt(data): 77 | """ eliminar de configberry la info de la seccion paxaprinter""" 78 | logger.info(f"SIO adopt: {data}") 79 | try: 80 | # Detener servicio de RabbitMQ 81 | self.rabbit_handler.stop() 82 | if self.config.delete_section("Paxaprinter"): 83 | logger.info("RabbitMQ service stopped and Paxaprinter section removed") 84 | except Exception as e: 85 | logger.error(f"adopt: {e}") 86 | 87 | 88 | @self.sio.event(namespace=ns) 89 | def message(data): 90 | logger.info(f"SIO message: {data}") 91 | if self.on_message: 92 | try: 93 | self.on_message(data) 94 | except Exception as e: 95 | logger.error(f"on_message callback: {e}") 96 | 97 | @self.sio.event(namespace=ns) 98 | def command(cfg: dict): 99 | logger.info(f"Vino evento command {cfg}") 100 | 101 | 102 | 103 | @self.sio.event(namespace=ns) 104 | def start_rabbit(cfg: dict): 105 | logger.info(f"start_rabbit: RabbitMQ {cfg}") 106 | 107 | # config + restart, pasamos la cola para tail -f 108 | # Create and start a daemon thread to run configure_and_restart 109 | self.rabbitmq_thread = threading.Thread( 110 | target=self.rabbit_handler.configure_and_restart, 111 | args=(cfg, self.message_queue), 112 | daemon=True 113 | ) 114 | self.rabbitmq_thread.start() 115 | self.rabbitmq_thread.join() 116 | 117 | def isRabbitMQRunning(self): 118 | """ 119 | Verifica si el hilo de RabbitMQ está en ejecución. 120 | """ 121 | if self.rabbitmq_thread and self.rabbitmq_thread.is_alive(): 122 | return True 123 | else: 124 | return False 125 | # Si no hay hilo, significa que RabbitMQ no está corriendo 126 | 127 | def isSioRunning(self): 128 | """ 129 | Verifica si el hilo de SIO está en ejecución. 130 | """ 131 | if self.thread and self.thread.is_alive(): 132 | return True 133 | else: 134 | return False 135 | # Si no hay hilo, significa que SIO no está corriendo 136 | 137 | def _run(self): 138 | try: 139 | logger.info(f"SIO run: {self.server_url}") 140 | self.sio.connect(self.server_url, namespaces=self.namespaces, headers={'x-uuid': self.uuid}) 141 | self.sio.wait() 142 | except Exception as e: 143 | logger.error(f"SIO Error al conectar: {e}") 144 | 145 | 146 | def start(self) -> threading.Thread: 147 | if self.thread and self.thread.is_alive(): 148 | return self.thread 149 | self.stop_event.clear() 150 | self.thread = threading.Thread(target=self._run, daemon=True) 151 | self.thread.start() 152 | 153 | return self.thread 154 | 155 | def stop(self, timeout=2): 156 | logger.info("SIO STOP") 157 | self.stop_event.set() 158 | try: 159 | self.sio.disconnect() # detenemios socketio 160 | self.rabbit_handler.stop() # detenemos RabbitMQ también 161 | except Exception as e: 162 | logger.error("Error al desconectar SIO o detener RabbitMQ: %s", e) 163 | finally: 164 | logger.info("SIO y RabbitMQ disconnected OK") 165 | 166 | if self.thread and self.thread.is_alive(): 167 | logger.info(f"SIO es STILL LIVE!! no deberia, Waiting for SIO thread to stop, timeout={timeout} seconds.") 168 | self.thread.join(timeout) 169 | if self.thread.is_alive(): 170 | logger.warning(f"SIO thread did not stop within the timeout period of {timeout} seconds.") 171 | self.thread = None 172 | 173 | if self.rabbitmq_thread and self.rabbitmq_thread.is_alive(): 174 | logger.info(f"RabbitMQ thread is STILL LIVE!! no deberia, Waiting for RabbitMQ thread to stop, timeout={timeout} seconds.") 175 | self.rabbitmq_thread.join(timeout) 176 | if self.rabbitmq_thread.is_alive(): 177 | logger.warning(f"RabbitMQ thread did not stop within the timeout period of {timeout} seconds.") 178 | self.rabbitmq_thread = None 179 | logger.info("SIO y RabbitMQ disconnected OK") 180 | 181 | return True -------------------------------------------------------------------------------- /src/fiscalberry/common/fiscalberry_logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import logging.config 5 | import tempfile 6 | import platform 7 | from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler 8 | from pathlib import Path 9 | import traceback 10 | 11 | # Evitar referencia circular en la importación 12 | def get_configberry(): 13 | from fiscalberry.common.Configberry import Configberry 14 | return Configberry() 15 | 16 | class FiscalberryLogger: 17 | """Sistema centralizado de logging para Fiscalberry.""" 18 | 19 | # Instancia singleton 20 | _instance = None 21 | _initialized = False 22 | 23 | @classmethod 24 | def get_instance(cls): 25 | if cls._instance is None: 26 | cls._instance = cls() 27 | return cls._instance 28 | 29 | def __init__(self): 30 | if FiscalberryLogger._initialized: 31 | return 32 | 33 | # Inicialización básica 34 | self.logger = logging.getLogger("Fiscalberry") 35 | self.logger.setLevel(logging.DEBUG) 36 | 37 | # Limpiar handlers existentes para evitar duplicados 38 | if self.logger.handlers: 39 | self.logger.handlers.clear() 40 | 41 | # Determinar ambiente y configurar nivel de detalle 42 | try: 43 | configberry = get_configberry() 44 | self.environment = configberry.config.get("SERVIDOR", "environment", fallback="production").lower() 45 | self.log_level = configberry.config.get("SERVIDOR", "log_level", fallback="INFO").upper() 46 | 47 | # Anunciar el modo de ejecución 48 | if self.environment == "development": 49 | print("* * * * * Modo de desarrollo * * * * *") 50 | else: 51 | print("@ @ @ @ @ Modo de producción @ @ @ @ @") 52 | except Exception as e: 53 | self.environment = "production" 54 | self.log_level = "INFO" 55 | print(f"Error al cargar configuración de logger: {e}") 56 | print(traceback.format_exc()) 57 | 58 | # Configurar rutas de log 59 | self.log_dir = self._determine_log_directory() 60 | self.log_file = os.path.join(self.log_dir, "fiscalberry.log") 61 | 62 | # Crear formateador de logs 63 | self.formatter = logging.Formatter( 64 | "%(asctime)s [%(levelname)s] [%(name)s] %(message)s", 65 | datefmt="%Y-%m-%d %H:%M:%S" 66 | ) 67 | 68 | # Configurar handlers 69 | self._setup_file_handler() 70 | self._setup_console_handler() 71 | 72 | # Configurar loggers de terceros 73 | self._configure_third_party_loggers() 74 | 75 | FiscalberryLogger._initialized = True 76 | 77 | # Auto-log de inicio 78 | self.logger.info(f"Sistema de logging inicializado en modo {self.environment}") 79 | self.logger.info(f"Archivos de log en: {self.log_file}") 80 | 81 | def _determine_log_directory(self): 82 | """Determina el directorio de logs según la plataforma.""" 83 | app_name = "fiscalberry" 84 | 85 | # Android 86 | if hasattr(os, 'getenv') and os.getenv('ANDROID_STORAGE') is not None: 87 | log_dir = os.path.join(os.getenv('EXTERNAL_STORAGE', '/sdcard'), app_name, 'logs') 88 | 89 | # Windows 90 | elif platform.system() == 'Windows': 91 | log_dir = os.path.join(os.getenv('LOCALAPPDATA', os.path.expanduser('~')), app_name, 'logs') 92 | 93 | # Linux y otros Unix 94 | else: 95 | log_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', app_name, 'logs') 96 | 97 | # Crear directorio si no existe 98 | try: 99 | os.makedirs(log_dir, exist_ok=True) 100 | except Exception as e: 101 | print(f"No se pudo crear directorio de logs: {e}") 102 | log_dir = tempfile.gettempdir() 103 | 104 | return log_dir 105 | 106 | def _setup_file_handler(self): 107 | """Configura el handler para archivo de logs.""" 108 | try: 109 | # RotatingFileHandler: máximo 5MB por archivo, con 5 backups 110 | file_handler = RotatingFileHandler( 111 | self.log_file, 112 | maxBytes=5*1024*1024, 113 | backupCount=5, 114 | encoding='utf-8' 115 | ) 116 | 117 | # Como alternativa, también puedes usar TimedRotatingFileHandler 118 | # file_handler = TimedRotatingFileHandler( 119 | # self.log_file, 120 | # when='midnight', 121 | # interval=1, 122 | # backupCount=30, 123 | # encoding='utf-8' 124 | # ) 125 | 126 | file_handler.setFormatter(self.formatter) 127 | file_handler.setLevel(getattr(logging, self.log_level)) 128 | self.logger.addHandler(file_handler) 129 | except Exception as e: 130 | print(f"Error al configurar file handler: {e}") 131 | print(traceback.format_exc()) 132 | 133 | def _setup_console_handler(self): 134 | """Configura el handler para salida a consola.""" 135 | try: 136 | console_handler = logging.StreamHandler(stream=sys.stdout) 137 | console_handler.setFormatter(self.formatter) 138 | 139 | # En desarrollo mostramos más información 140 | if self.environment == "development": 141 | console_level = logging.DEBUG 142 | else: 143 | console_level = logging.WARNING 144 | 145 | console_handler.setLevel(console_level) 146 | self.logger.addHandler(console_handler) 147 | except Exception as e: 148 | print(f"Error al configurar console handler: {e}") 149 | 150 | def _configure_third_party_loggers(self): 151 | """Configura niveles para loggers de librerías externas.""" 152 | # Reducir el ruido de logs de librerías externas 153 | logging.getLogger("pika").setLevel(logging.WARNING) 154 | logging.getLogger("socketio").setLevel(logging.INFO) 155 | logging.getLogger("requests").setLevel(logging.WARNING) 156 | logging.getLogger("urllib3").setLevel(logging.WARNING) 157 | 158 | def get_logger(self, name=None): 159 | """Devuelve el logger, opcionalmente con un nombre específico.""" 160 | if name: 161 | return logging.getLogger(f"Fiscalberry.{name}") 162 | return self.logger 163 | 164 | def get_log_file_path(self): 165 | """Devuelve la ruta del archivo de log.""" 166 | return self.log_file 167 | 168 | # API pública simplificada 169 | def getLogger(name=None): 170 | """Obtiene el logger global o uno específico por nombre.""" 171 | logger_instance = FiscalberryLogger.get_instance() 172 | return logger_instance.get_logger(name) 173 | 174 | def getLogFilePath(): 175 | """Devuelve la ruta del archivo de log.""" 176 | logger_instance = FiscalberryLogger.get_instance() 177 | return logger_instance.get_log_file_path() 178 | 179 | # Para mantener compatibilidad con el código existente 180 | Logger = getLogger() -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # ¿Para qué sirve? 3 | 4 | Para enviar un JSON (mediante websocket), que fiscalberry lo reciba, lo transforme en un conjunto de comandos compatible con la impresora instalada, conecte con la impresora y responda al websocket con la respuesta que nos envió la impresora. 5 | 6 | Descarga la última versión socketio para windows 7 | 8 | [fiscalberry-win]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-win.exe)) 9 | 10 | Para Linux 11 | [fiscalberry-lin]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-lin)) 12 | 13 | Para Raspberry pi 14 | [fiscalberry-lin]([https://github.com/paxapos/fiscalberry/releases/latest/download/fiscalberry-pi)) 15 | 16 | # Como comenzar (solo developers del proyecto) 17 | 18 | fiscalberry se puede instalar para usar con UI o solo consola (por ejemplo para ser ejecutado en una raspberry sin UI) 19 | 20 | la forma mas simple es descargar el proyecto 21 | y ejecutar asi: 22 | pip install -e . 23 | y luego podras ejecutar fiscalberry_gui o fiscalberry_cli como un comando mas 24 | 25 | si lo queres instalar en prod seria sin el -e 26 | pip install . 27 | 28 | la otra opcion mas para DEVs seria crear un virtual environment: 29 | 30 | clonar repo 31 | crear enviroment (1 para kivy y otro para modo cli, consola) 32 | python3 -m venv .venv.kivy 33 | python3 -m venv .venv.cli 34 | 35 | luego activar el enviroment que se desea usar, por ejemplo modo solo consola: 36 | source venv.cli/bin/activate 37 | 38 | instalar requerimientos de modo consola 39 | pip install -r requirements.cli.txt 40 | 41 | ahora ya puede ejecutar: 42 | python src/cli.py 43 | 44 | lo mismo si se desea trabajar con interfaz kivy... 45 | 46 | 47 | # ¿Qué es? 48 | 49 | Fiscalberry es un servidor de websockets desarrollado en Python pensado para que corra en una raspberry-pi (de ahí viene el nombre de este proyecto). **Pero funciona perfectamente en otros sistemas operativos.** 50 | ![fiscalberry JSON](http://alevilar.com/uploads/entendiendo%20fiscalberry.jpg) 51 | 52 | # ¿Qué impresoras son compatibles? 53 | 54 | Fiscalberry tiene drivers desarrollados para conectarse con 2 tipos de impresoras: Fiscales y Receipt. 55 | 56 | Impresoras Fiscales compatibles: Hasar y Epson 57 | 58 | Nueva versión con soporte para las últimas impresoras Fiscales de segunda generación (2gen) HASAR y EPSON
59 | Modelos compatibles (HASAR: SMH/PT-250F, EPSON: TM-T900FA) 60 | 61 | Impresoras Receipt (de comandas) compatibles: las que soportan ESC/P 62 | 63 | ## Fiscalberry como servidor de impresión (print-server) de impresoras receipt (comanderas) y fiscales 64 | 65 | Fiscalberry es un 3x1, actúa como: protocolo, servidor y driver facilitando al programador la impresión de tickets, facturas o comprobantes fiscales. 66 | 67 | - _PROTOCOLO_: Siguiendo la estructura del JSON indicado, se podrá imprimir independientemente de la impresora conectada. Fiscalberry se encargará de conectarse y pelear con los códigos y comandos especiales de cada marca/modelo. 68 | - _SERVIDOR_: gracias al servidor de websockets es posible conectar tu aplicación para que ésta fácilmente pueda enviar JSON's y recibir las respuestas de manera asíncrona. 69 | - _DRIVER_: Es el encargado de transformar el JSON genérico en un conjunto de comandos especiales según marca y modelo de la impresora. Aquí es donde se adaptó el código del proyecto de Reingart () para impresoras Hasar y Epson. 70 | 71 | Funciona en cualquier PC con cualquier sistema operativo que soporte Python. 72 | 73 | La idea original fue pensada para que funcione en una raspberry pi, cuyo fin es integrar las fiscales al mundo de la Internet de las Cosas (IOT). 74 | 75 | ## ¿Para quienes está pensado? 76 | 77 | Para los desarrolladores que desean enviar a imprimir mediante JSON (es decir, desde algún lugar de la red, internet, intranet, etc, etc) de una forma "estándar" y que funcione en cualquier impresora, marca y modelo. 78 | 79 | ## PROBALO 80 | 81 | ### Descargar 82 | 83 | usando git 84 | 85 | ```sh 86 | git clone https://github.com/paxapos/fiscalberry.git 87 | ``` 88 | 89 | o directamente el ZIP: 90 | 91 | ### Instalar Dependencias 92 | 93 | ATENCIÓN: Funciona con Python 2.7.* NO en Python 3! 94 | 95 | probado bajo python 2.7.6 en Linux, Raspian, Ubuntu, Open Suse y Windows 96 | 97 | Se necesitan varias dependencias: 98 | 99 | ```sh 100 | sudo pip install -r requirements.txt 101 | 102 | ``` 103 | 104 | Si te encontras con el error "socket.gaierror: Name or service not known" 105 | 106 | A veces, en Linux (Open Suse), ser necesario poner el nombre del equipo (hostname) en el archivo /etc/hosts, si es que aún no lo tenías. 107 | Generalmente el archivo hosts viene solo con la dirección "127.0.0.1 localhost", 108 | 109 | para solucionarlo debés ejecutar el comando 110 | 111 | ```bash 112 | hostname 113 | ``` 114 | 115 | y ver cuál es el nombre de la máquina para agregarlo al archivo /etc/hosts 116 | 127.0.0.1 nombre-PC localhost 117 | 118 | ### Iniciar el programa 119 | 120 | ```sh 121 | sudo python server.py 122 | 123 | # o iniciar como demonio linux 124 | sudo python rundaemon.py 125 | ``` 126 | 127 | Ahora ya puedes conectarte en el puerto 12000 128 | entrando a un browser y la dirección 129 | 130 | ## Conceptos básicos ¿Cómo funciona? 131 | 132 | Supongamos que tenemos este JSON genérico: 133 | 134 | ``` 135 | { 136 | "ACCION_A_EJECUTAR": { 137 | PARAMETROS_DE_LA_ACCION 138 | ... 139 | } 140 | } 141 | ``` 142 | 143 | Lo enviamos usando websockets a un host y puerto determinado (el servidor fiscalberry), éste lo procesa, envía a imprimir, y responde al cliente con la respuesta de la impresora. Por ejemplo, devolviendo el número del último comprobante impreso. 144 | 145 | Otro ejemplo más concreto: queremos imprimir un ticket, esta acción en el protocolo fiscalberry se lo llama como acción "printTicket" y está compuesta de 2 parámetros obligatorios: "encabezado" e "items". 146 | 147 | El "encabezado" indica el tipo de comprobante a imprimir (y también podría agregarle datos del cliente, que son opcionales). 148 | Los ítems son una lista de productos a imprimir donde, en este ejemplo, tenemos una coca cola, con impuesto de 21%, importe $10, descripción del producto "COCA COLA" y la cantidad vendida es 2. 149 | 150 | ```json 151 | { 152 | "printTicket": { 153 | "encabezado": { 154 | "tipo_cbte": "T", // tipo tiquet *obligatorio 155 | }, 156 | "items": [ 157 | { 158 | "alic_iva": 21.0, // impuesto 159 | "importe": 10, // importe 160 | "ds": "COCA COLA", // descripcion producto 161 | "qty": 2.0 // cantidad 162 | } 163 | ] 164 | } 165 | } 166 | ``` 167 | 168 | ### JSON RESPUESTA 169 | 170 | Existen 2 tipos de respuesta y siempre vienen con la forma de un JSON. 171 | 172 | Aquellos que son una respuesta a un comando enviado, comienzan con "ret" 173 | **_{"ret": ......}_** 174 | 175 | Aquellos que son un mensaje directo de algun dispositivo conectado, vienen con "msg" 176 | 177 | **_{"msg": ......}_** 178 | 179 | ```javascript 180 | // ejemplo retorno de un mensaje cuando no hay papel 181 | {"msg": ["Poco papel para comprobantes o tickets"]} 182 | ``` 183 | 184 | #### NOTA 185 | 186 | Deberás enviar JSON válidos al servidor. Recomendamos usar la pagina para verificar como tu programa esta generando los JSON. 187 | -------------------------------------------------------------------------------- /.github/workflows/desktop.build.yml: -------------------------------------------------------------------------------- 1 | name: Build Desktop & Pi Ejecutables - on New Version 2 | # 3 | # Este workflow construye ejecutables para múltiples plataformas: 4 | # - CLI: Linux x64, Windows x64, Raspberry Pi ARM64 5 | # - GUI: Solo Windows x64 (Kivy es más complejo en Linux) 6 | # 7 | 8 | on: 9 | push: 10 | tags: # Trigger on tags like v*.*.* 11 | - 'v*.*.*' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true # Cancel previous runs if new commits are pushed 16 | 17 | jobs: 18 | build-desktop: 19 | strategy: 20 | fail-fast: false # Continue with other jobs if one fails 21 | matrix: 22 | os: [ubuntu-22.04, windows-latest] 23 | include: 24 | - os: ubuntu-22.04 25 | os_name: linux 26 | asset_suffix_cli: cli-lin-x64 27 | artifact_path_cli: dist/fiscalberry-cli 28 | python_platform: linux 29 | build_gui: false 30 | - os: windows-latest 31 | os_name: windows 32 | asset_suffix_cli: cli-win-x64.exe 33 | asset_suffix_gui: gui-win-x64.exe 34 | artifact_path_cli: dist/fiscalberry-cli.exe 35 | artifact_path_gui: dist/fiscalberry-gui.exe 36 | python_platform: win32 37 | build_gui: true 38 | 39 | 40 | runs-on: ${{ matrix.os }} 41 | 42 | steps: 43 | 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: '3.12' 51 | 52 | - name: Install Linux dependencies (if applicable) 53 | if: runner.os == 'Linux' 54 | run: | 55 | sudo apt-get update 56 | sudo apt-get install -y \ 57 | libcups2-dev \ 58 | libgl1-mesa-glx libgles2-mesa libegl1-mesa libmtdev1 \ 59 | xvfb 60 | 61 | # Add steps for macOS dependencies if needed (e.g., using brew) 62 | # - name: Install macOS dependencies (if applicable) 63 | # if: runner.os == 'macOS' 64 | # run: | 65 | # brew install sdl2 # Example 66 | 67 | - name: Cache PyInstaller 68 | uses: actions/cache@v4 69 | with: 70 | path: | 71 | ~/.cache/pip 72 | ~/.cache/pyinstaller 73 | ~/AppData/Local/pip/Cache # Windows cache 74 | key: ${{ runner.os }}-pyinstaller-${{ hashFiles('fiscalberry-gui.spec') }} 75 | restore-keys: | 76 | ${{ runner.os }}-pyinstaller- 77 | 78 | - name: Install PyInstaller 79 | run: python -m pip install --upgrade pip pyinstaller 80 | 81 | # --- Build CLI for all platforms --- 82 | - name: Install CLI dependencies 83 | run: pip install -r requirements.cli.txt 84 | 85 | - name: Build CLI executable (${{ matrix.os_name }}) 86 | shell: bash 87 | run: | 88 | echo "Building CLI executable for ${{ matrix.os_name }}..." 89 | pyinstaller fiscalberry-cli.spec 90 | 91 | # --- Build GUI (only for Windows) --- 92 | - name: Install GUI dependencies (Windows only) 93 | if: matrix.build_gui == true 94 | run: pip install -r requirements.kivy.txt 95 | 96 | - name: Build GUI executable (Windows only) 97 | if: matrix.build_gui == true 98 | shell: bash 99 | run: | 100 | echo "Building GUI executable for Windows..." 101 | # Set environment variables to prevent Kivy from trying to initialize graphics 102 | export KIVY_NO_WINDOW=1 103 | export KIVY_GL_BACKEND=mock 104 | export KIVY_GRAPHICS=mock 105 | pyinstaller fiscalberry-gui.spec 106 | 107 | - name: List dist directory contents (after build) 108 | if: always() 109 | shell: bash 110 | run: | 111 | echo "Listing contents of ./dist directory:" 112 | ls -lR ./dist || echo "Dist directory not found or empty." 113 | 114 | - name: Rename CLI artifact 115 | shell: bash 116 | run: | 117 | mkdir -p artifacts 118 | # Ensure the CLI source path exists before moving 119 | if [ -e "${{ matrix.artifact_path_cli }}" ]; then 120 | echo "Moving ${{ matrix.artifact_path_cli }} to artifacts/fiscalberry-${{ matrix.asset_suffix_cli }}" 121 | mv ${{ matrix.artifact_path_cli }} artifacts/fiscalberry-${{ matrix.asset_suffix_cli }} 122 | else 123 | echo "ERROR: CLI source file ${{ matrix.artifact_path_cli }} not found!" 124 | exit 1 125 | fi 126 | 127 | - name: Rename GUI artifact (Windows only) 128 | if: matrix.build_gui == true 129 | shell: bash 130 | run: | 131 | # Ensure the GUI source path exists before moving 132 | if [ -e "${{ matrix.artifact_path_gui }}" ]; then 133 | echo "Moving ${{ matrix.artifact_path_gui }} to artifacts/fiscalberry-${{ matrix.asset_suffix_gui }}" 134 | mv ${{ matrix.artifact_path_gui }} artifacts/fiscalberry-${{ matrix.asset_suffix_gui }} 135 | else 136 | echo "ERROR: GUI source file ${{ matrix.artifact_path_gui }} not found!" 137 | exit 1 138 | fi 139 | 140 | - name: Upload executables artifact (${{ matrix.os_name }}) 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: fiscalberry-executables-${{ matrix.os_name }} 144 | path: artifacts/ 145 | 146 | build-raspberry-pi-cli: 147 | runs-on: ubuntu-22.04 148 | steps: 149 | - name: Checkout code 150 | uses: actions/checkout@v4 151 | 152 | - name: Build Pi CLI executable 153 | uses: uraimo/run-on-arch-action@v2 154 | with: 155 | arch: aarch64 156 | distro: ubuntu22.04 157 | run: | 158 | apt-get update 159 | apt-get install -y python3-pip python3-dev libcups2-dev 160 | # Install CLI dependencies only 161 | pip3 install -r requirements.cli.txt 162 | pip3 install pyinstaller 163 | pyinstaller fiscalberry-cli.spec 164 | 165 | - name: Rename Pi CLI artifact 166 | run: | 167 | mkdir -p artifacts 168 | sudo mv dist/fiscalberry-cli artifacts/fiscalberry-cli-pi-arm64 169 | 170 | - name: Upload Pi CLI executable artifact 171 | uses: actions/upload-artifact@v4 172 | with: 173 | name: fiscalberry-executables-pi 174 | path: artifacts/fiscalberry-cli-pi-arm64 175 | 176 | create-release: 177 | runs-on: ubuntu-22.04 178 | needs: [build-desktop, build-raspberry-pi-cli] # Wait for all builds 179 | permissions: 180 | contents: write # Needed to create release and upload assets 181 | 182 | steps: 183 | - name: Checkout code (optional, needed if release notes use repo files) 184 | uses: actions/checkout@v4 185 | 186 | - name: Download all artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | path: ./artifacts # Download all artifacts into artifacts directory 190 | # No 'name' specified to download all artifacts from the workflow run 191 | 192 | - name: List downloaded artifacts (debugging) 193 | run: | 194 | find ./artifacts -type f 195 | 196 | - name: Create GitHub Release 197 | id: create_release 198 | uses: actions/create-release@v1 199 | env: 200 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 201 | with: 202 | tag_name: ${{ github.ref_name }} # Use the tag that triggered the workflow 203 | release_name: Release ${{ github.ref_name }} 204 | draft: false 205 | prerelease: false 206 | # body: | # Optional: Add release notes 207 | # Release notes for ${{ github.ref_name }} 208 | # - Feature A 209 | # - Bugfix B 210 | 211 | - name: Set upload URL 212 | run: echo "UPLOAD_URL=${{ steps.create_release.outputs.upload_url }}" >> $GITHUB_ENV 213 | 214 | - name: Upload all assets to release 215 | shell: bash 216 | run: | 217 | for artifact_dir in ./artifacts/fiscalberry-executables-*; do 218 | files=$(find "$artifact_dir" -type f | tr '\n' ' ') 219 | if [ -n "$files" ]; then 220 | echo "Uploading files from $artifact_dir" 221 | gh release upload "${{ github.ref_name }}" $files --clobber -R "${{ github.repository }}" 222 | fi 223 | done 224 | env: 225 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 226 | -------------------------------------------------------------------------------- /src/fiscalberry/common/rabbitmq/consumer.py: -------------------------------------------------------------------------------- 1 | import os, pika, traceback 2 | import queue 3 | 4 | from fiscalberry.common.ComandosHandler import ComandosHandler, TraductorException 5 | from fiscalberry.common.fiscalberry_logger import getLogger 6 | from typing import Optional 7 | 8 | 9 | class RabbitMQConsumer: 10 | 11 | STREAM_NAME = "paxaprinter" 12 | 13 | # 5GB 14 | STREAM_RETENTION = 5000000000 15 | 16 | def __init__( 17 | self, 18 | host: str, 19 | port: int, 20 | user: str, 21 | password: str, 22 | queue_name: str, 23 | message_queue: Optional[queue.Queue] = None, 24 | vhost: str = "/" 25 | ) -> None: 26 | self.host = host 27 | self.port = port 28 | self.user = user 29 | self.vhost = vhost 30 | self.password = password 31 | self.connection = None 32 | self.channel = None 33 | self.queue = queue_name 34 | self.message_queue = message_queue 35 | 36 | # Configuro logger según ambiente 37 | environment = os.getenv('ENVIRONMENT', 'production') 38 | if environment == 'development': 39 | sioLogger = True 40 | else: 41 | sioLogger = False 42 | 43 | self.logger = getLogger() 44 | 45 | 46 | def connect(self): 47 | """Conecta al servidor RabbitMQ.""" 48 | self.logger.info(f"Connecting to RabbitMQ server: {self.host}:{self.port}") 49 | 50 | # Use connection parameters with shorter timeouts for faster failure detection 51 | params = pika.ConnectionParameters( 52 | host=self.host, 53 | port=self.port, 54 | virtual_host=self.vhost, 55 | credentials=pika.PlainCredentials(self.user, self.password), 56 | heartbeat=600, 57 | blocked_connection_timeout=300, 58 | socket_timeout=10, # Timeout más corto para detectar errores de red rápidamente 59 | connection_attempts=1, # Solo un intento, el retry lo maneja process_handler 60 | retry_delay=1 # Delay corto entre intentos 61 | ) 62 | self.connection = pika.BlockingConnection(params) 63 | self.channel = self.connection.channel() 64 | self.logger.info(f"Blockin channel creado") 65 | # Manejo del exchange 66 | try: 67 | self.logger.info(f"Probando si existe exchange") 68 | # Verificar si el exchange existe sin intentar modificarlo 69 | self.channel.exchange_declare( 70 | exchange=self.STREAM_NAME, 71 | passive=True # Solo verifica si existe, no intenta declararlo 72 | ) 73 | self.logger.info(f"Exchange {self.STREAM_NAME} ya existe") 74 | except pika.exceptions.ChannelClosedByBroker: 75 | self.logger.info(f"bloquin contection magic pufff") 76 | # Si el exchange no existe, reconectamos y lo creamos 77 | self.connection = pika.BlockingConnection(params) 78 | self.channel = self.connection.channel() 79 | 80 | self.channel.exchange_declare( 81 | exchange=self.STREAM_NAME, 82 | exchange_type='direct', # Usar direct como tipo predeterminado 83 | durable=True 84 | ) 85 | self.logger.info(f"Exchange {self.STREAM_NAME} creado como direct") 86 | 87 | self.logger.info(f"Conectado con RabbitMQ, ahora ... Conectando a cola: {self.queue}") 88 | 89 | # Manejo de la cola - verificar primero si existe 90 | try: 91 | # Verificar si la cola existe sin intentar modificarla 92 | self.channel.queue_declare( 93 | queue=self.queue, 94 | passive=True # Solo verifica si existe, no intenta declararla 95 | ) 96 | self.logger.info(f"Cola {self.queue} ya existe, usando configuración existente") 97 | except pika.exceptions.ChannelClosedByBroker: 98 | # Si la cola no existe o hay problemas con el canal, reconectamos 99 | self.connection = pika.BlockingConnection(params) 100 | self.channel = self.connection.channel() 101 | 102 | # Declarar la cola ahora que sabemos que no existe o fue eliminada 103 | self.channel.queue_declare( 104 | queue=self.queue, 105 | durable=True # Crear como durable si es una nueva cola 106 | ) 107 | self.logger.info(f"Cola {self.queue} creada como durable") 108 | 109 | # Binding - intentar con la cola como routing key 110 | try: 111 | self.channel.queue_bind( 112 | exchange=self.STREAM_NAME, 113 | queue=self.queue, 114 | routing_key=self.queue # Usar el nombre de la cola como routing key 115 | ) 116 | self.logger.info(f"Cola {self.queue} enlazada a exchange {self.STREAM_NAME}") 117 | except pika.exceptions.ChannelClosedByBroker as e: 118 | self.logger.error(f"Error al enlazar cola: {e}") 119 | # Reconectar para seguir operando 120 | self.connection = pika.BlockingConnection(params) 121 | self.channel = self.connection.channel() 122 | 123 | self.logger.info(f"Connected to RabbitMQ and bound queue {self.queue} to exchange {self.STREAM_NAME}") 124 | 125 | 126 | def start(self): 127 | self.connect() 128 | 129 | def callback(ch, method, properties, body): 130 | self.logger.info(f"Received message in queue {self.queue}") 131 | 132 | try: 133 | # Parse the message body - it might be bytes and need decoding 134 | if isinstance(body, bytes): 135 | body_str = body.decode('utf-8') 136 | else: 137 | body_str = body 138 | 139 | # Log the raw message for debugging 140 | self.logger.info(f"Message content: {body_str[:100]}...") # Log first 100 chars 141 | 142 | # Try to parse as JSON if needed 143 | try: 144 | import json 145 | json_data = json.loads(body_str) 146 | self.logger.info("Successfully parsed message as JSON") 147 | except json.JSONDecodeError: 148 | self.logger.warning("Message is not valid JSON, passing as raw string") 149 | json_data = body_str 150 | 151 | # Process the message 152 | comandoHandler = ComandosHandler() 153 | result = comandoHandler.send_command(json_data) 154 | 155 | # Verificar si hay un error en la respuesta 156 | if "err" in result: 157 | self.logger.error(f"Error in command handler: {result['err']}") 158 | # Si es un error recuperable, podríamos reintentar o poner en una cola de espera 159 | # Por ahora, consideramos que es un error y no reconocemos el mensaje 160 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) 161 | return 162 | 163 | self.logger.info(f"Command handler result: {result}") 164 | 165 | # Acknowledge the message ONLY after successful processing 166 | ch.basic_ack(delivery_tag=method.delivery_tag) 167 | 168 | except TraductorException as e: 169 | self.logger.error(f"TraductorException Error: {e}") 170 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) 171 | self.logger.error(traceback.format_exc()) 172 | except Exception as e: 173 | self.logger.error(f"Error processing message: {e}") 174 | self.logger.error(traceback.format_exc()) 175 | ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) 176 | 177 | 178 | 179 | # Set prefetch count to process one message at a time 180 | self.channel.basic_qos(prefetch_count=1) 181 | self.channel.basic_consume(queue=self.queue, on_message_callback=callback, auto_ack=False) 182 | self.logger.info(f"Waiting for messages in queue: {self.queue}") 183 | 184 | try: 185 | self.channel.start_consuming() 186 | except Exception as e: 187 | self.logger.error(f"Error in consumer: {e}") 188 | self.logger.error(traceback.format_exc()) 189 | # Try to reconnect 190 | self.stop() 191 | raise 192 | 193 | 194 | def stop(self): 195 | """Detiene la conexión.""" 196 | if self.connection: 197 | self.connection.close() 198 | 199 | -------------------------------------------------------------------------------- /src/fiscalberry/ui/fiscalberry_app.py: -------------------------------------------------------------------------------- 1 | from kivy.app import App 2 | from kivy.uix.screenmanager import ScreenManager 3 | from fiscalberry.ui.main_screen import MainScreen 4 | from fiscalberry.ui.login_screen import LoginScreen 5 | from fiscalberry.ui.adopt_screen import AdoptScreen 6 | from kivy.clock import Clock, mainthread 7 | import threading 8 | import queue 9 | from fiscalberry.common.Configberry import Configberry 10 | from fiscalberry.common.service_controller import ServiceController 11 | from kivy.lang import Builder 12 | from kivy.properties import StringProperty, BooleanProperty 13 | from fiscalberry.ui.log_screen import LogScreen 14 | from threading import Thread 15 | import time 16 | import sys 17 | import os 18 | import signal 19 | 20 | 21 | from fiscalberry.version import VERSION 22 | 23 | class FiscalberryApp(App): 24 | name = StringProperty("Servidor de Impresión") 25 | uuid = StringProperty("") 26 | host = StringProperty("") 27 | tenant = StringProperty("") 28 | siteName = StringProperty("") 29 | siteAlias = StringProperty("") 30 | version = StringProperty( VERSION ) 31 | 32 | 33 | assetpath = os.path.join(os.path.dirname(__file__), "assets") 34 | 35 | background_image = StringProperty(os.path.join(assetpath, "bg.jpg")) 36 | logo_image = StringProperty(os.path.join(assetpath, "fiscalberry.png")) 37 | disconnected_image = StringProperty(os.path.join(assetpath, "disconnected.png")) 38 | connected_image = StringProperty(os.path.join(assetpath, "connected.png")) 39 | 40 | sioConnected: bool = BooleanProperty(False) 41 | rabbitMqConnected: bool = BooleanProperty(False) 42 | 43 | status_message = StringProperty("Esperando conexión...") 44 | 45 | def __init__(self, **kwargs): 46 | super().__init__(**kwargs) 47 | self.message_queue = queue.Queue() # Inicializar la variable message_queue aquí 48 | 49 | self._service_controller = ServiceController() 50 | self._configberry = Configberry() 51 | self._stopping = False # Bandera para evitar múltiples llamadas de cierre 52 | 53 | self.updatePropertiesWithConfig() 54 | 55 | # Programar la verificación del estado de SocketIO cada 2 segundos 56 | Clock.schedule_interval(self._check_sio_status, 2) 57 | Clock.schedule_interval(self._check_rabbit_status, 2) 58 | 59 | def updatePropertiesWithConfig(self): 60 | """ 61 | Actualiza las propiedades de la aplicación con los valores de configuración. 62 | """ 63 | self.uuid = self._configberry.get("SERVIDOR", "uuid", fallback="") 64 | self.host = self._configberry.get("SERVIDOR", "sio_host", fallback="") 65 | self.tenant = self._configberry.get("Paxaprinter", "tenant", fallback="") 66 | self.siteName = self._configberry.get("Paxaprinter", "site_name", fallback="") 67 | self.siteAlias = self._configberry.get("Paxaprinter", "alias", fallback="") 68 | 69 | def _check_sio_status(self, dt): 70 | """Verifica el estado de la conexión SocketIO y actualiza la propiedad.""" 71 | self.sioConnected = self._service_controller.isSocketIORunning() 72 | # Opcionalmente, puedes actualizar el status_message aquí también si lo deseas 73 | # if self.sioConnected: 74 | # self.status_message = "Conectado a SocketIO" 75 | # else: 76 | # self.status_message = "Esperando conexión SocketIO..." 77 | 78 | def _check_rabbit_status(self, dt): 79 | """Verifica el estado de la conexión RabbitMQ y actualiza la propiedad.""" 80 | self.rabbitMqConnected = self._service_controller.isRabbitRunning() 81 | # Opcionalmente, puedes actualizar el status_message aquí también si lo deseas 82 | # if self.rabbitMqConnected: 83 | # self.status_message = "Conectado a RabbitMQ" 84 | # else: 85 | # self.status_message = "Esperando conexión RabbitMQ..." 86 | 87 | @mainthread # Decorar el método 88 | def _on_config_change(self, data): 89 | """ 90 | Callback que se llama cuando hay un cambio en la configuración. 91 | hay que renderizar las pantallas de nuevo para que se vean los cambios. 92 | """ 93 | 94 | self.updatePropertiesWithConfig() 95 | 96 | 97 | 98 | # Usar el ScreenManager existente (self.root) 99 | sm = self.root 100 | if sm: # Verificar que self.root ya existe 101 | if self.tenant: 102 | # Si hay un tenant, ir a la pantalla principal 103 | if sm.has_screen("main"): 104 | sm.current = "main" 105 | else: 106 | print("Advertencia: No se encontró la pantalla 'main'.") 107 | else: 108 | # Si no hay tenant, ir a la pantalla de adopción 109 | if sm.has_screen("adopt"): 110 | sm.current = "adopt" 111 | self.on_stop_service() # Detener servicio si se des-adopta 112 | 113 | else: 114 | print("Advertencia: No se encontró la pantalla 'adopt'.") 115 | else: 116 | print("Error: self.root (ScreenManager) aún no está disponible.") 117 | 118 | 119 | def on_toggle_service(self): 120 | """Llamado desde la GUI para alternar el estado del servicio.""" 121 | if self._service_controller.is_service_running(): 122 | # Si el servicio está corriendo, detenerlo 123 | self.on_stop_service() 124 | else: 125 | # Si el servicio no está corriendo, iniciarlo 126 | self.on_start_service() 127 | 128 | 129 | def on_start_service(self): 130 | """Llamado desde la GUI para iniciar el servicio.""" 131 | Thread(target=self._service_controller.start, daemon=True).start() 132 | 133 | 134 | def on_stop_service(self): 135 | """Llamado desde la GUI para detener el servicio.""" 136 | # Evitar múltiples llamadas 137 | if self._stopping: 138 | return 139 | 140 | print("Deteniendo servicios desde la GUI...") 141 | 142 | # Solo detener los servicios, no forzar salida de la aplicación 143 | self._service_controller._stop_services_only() 144 | print("Servicios detenidos desde la GUI") 145 | 146 | 147 | def on_stop(self): 148 | # Este método se llama al cerrar la aplicación 149 | if self._stopping: 150 | return 151 | 152 | print("Cerrando aplicación, deteniendo servicios...") 153 | self._stopping = True 154 | 155 | # Solo detener los servicios, no llamar a app.stop() para evitar recursión 156 | self._service_controller._stop_services_only() 157 | print("Servicios detenidos...") 158 | 159 | # Usar múltiples estrategias para forzar la salida 160 | Clock.schedule_once(self._force_exit, 0.1) 161 | 162 | def _force_exit(self, dt): 163 | """Método auxiliar para forzar la salida después de la limpieza.""" 164 | import signal 165 | import threading 166 | 167 | print("Forzando salida de la aplicación...") 168 | 169 | # Estrategia 1: Detener el Clock de Kivy 170 | try: 171 | Clock.stop() 172 | except: 173 | pass 174 | 175 | # Estrategia 2: Terminar threads activos 176 | try: 177 | for thread in threading.enumerate(): 178 | if thread != threading.current_thread() and thread.is_alive(): 179 | if hasattr(thread, '_stop'): 180 | thread._stop() 181 | except: 182 | pass 183 | 184 | # Estrategia 3: Usar signal para terminar el proceso 185 | try: 186 | os.kill(os.getpid(), signal.SIGTERM) 187 | except: 188 | pass 189 | 190 | # Estrategia 4: Exit forzado como último recurso 191 | os._exit(0) 192 | 193 | 194 | def build(self): 195 | self.title = "Servidor de Impresión" 196 | 197 | self.icon = os.path.join(os.path.dirname(__file__), "assets", "fiscalberry.png") 198 | 199 | # escuchar cambios en configberry 200 | self._configberry.add_listener(self._on_config_change) 201 | 202 | self.on_start_service() 203 | 204 | # Cargar el archivo KV 205 | kv_path = os.path.join(os.path.dirname(__file__), "kv", "main.kv") 206 | Builder.load_file(kv_path) 207 | 208 | sm = ScreenManager() 209 | #login_screen = LoginScreen(name="login") 210 | #sm.add_widget(login_screen) 211 | sm.add_widget(AdoptScreen(name="adopt")) 212 | sm.add_widget(MainScreen(name="main")) 213 | 214 | # Agregar la pantalla de logs 215 | log_screen = LogScreen(name="logs") 216 | sm.add_widget(log_screen) 217 | 218 | # Verificar ya tiene tenant o debe ser adoptado 219 | if self.tenant: 220 | # Si hay un tenant, ir directamente a la pantalla principal 221 | sm.current = "main" 222 | else: 223 | # Si no hay token, mostrar la pantalla de adoptar 224 | sm.current = "adopt" 225 | 226 | return sm 227 | 228 | if __name__ == "__main__": 229 | FiscalberryApp().run() -------------------------------------------------------------------------------- /src/fiscalberry/common/rabbitmq/process_handler.py.backup: -------------------------------------------------------------------------------- 1 | import threading 2 | import socket 3 | import time 4 | import logging 5 | def _run_consumer(self, host, port, user, password, queue_name, message_queue): 6 | """Loop de conexión/reconexión al servidor RabbitMQ con backoff exponencial.""" 7 | retry_count = 0 8 | max_retries_before_backoff = 3 9 | base_delay = 5 # segundos 10 | max_delay = 300 # 5 minutos máximo 11 | 12 | while not self._stop_event.is_set(): 13 | try: 14 | consumer = RabbitMQConsumer( 15 | host=host, 16 | port=port, 17 | user=user, 18 | password=password, 19 | queue_name=queue_name, 20 | message_queue=message_queue 21 | ) 22 | consumer.start() 23 | # Si llegamos aquí, la conexión fue exitosa, resetear contador 24 | retry_count = 0 25 | logger.info("Conexión RabbitMQ establecida exitosamente") 26 | # consumer.start() sólo retorna al desconectarse o error. 27 | except socket.gaierror as ex: 28 | retry_count += 1 29 | logger.error(f"Error de resolución DNS para '{host}': {ex}") 30 | logger.error("Posibles soluciones:") 31 | logger.error("1. Verificar que el hostname 'rabbitmq' esté configurado correctamente") 32 | logger.error("2. Verificar conectividad de red") 33 | logger.error("3. Verificar que el servidor RabbitMQ esté ejecutándose") 34 | logger.error("4. Considerar usar una IP directa en lugar del hostname") 35 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 36 | except pika.exceptions.AMQPConnectionError as ex: 37 | retry_count += 1 38 | logger.error(f"Error de conexión AMQP a {host}:{port}: {ex}") 39 | logger.error("Verificar que el servidor RabbitMQ esté ejecutándose y accesible") 40 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 41 | except pika.exceptions.ProbableAuthenticationError as ex: 42 | retry_count += 1 43 | logger.error(f"Error de autenticación con RabbitMQ: {ex}") 44 | logger.error(f"Verificar credenciales - Usuario: {user}, VHost: {self.config.get('RabbitMq', 'vhost', '/')}") 45 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 46 | except pika.exceptions.ProbableAccessDeniedError as ex: 47 | retry_count += 1 48 | logger.error(f"Acceso denegado a RabbitMQ: {ex}") 49 | logger.error(f"Verificar permisos del usuario '{user}' en vhost '{self.config.get('RabbitMq', 'vhost', '/')}'") 50 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 51 | except ConnectionError as ex: 52 | retry_count += 1 53 | logger.error(f"Error de conexión de red a {host}:{port}: {ex}") 54 | logger.error("Verificar conectividad de red y que el puerto esté abierto") 55 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 56 | except Exception as ex: 57 | retry_count += 1 58 | logger.error(f"Error inesperado en RabbitMQConsumer: {ex}", exc_info=True) 59 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 60 | 61 | logger.info("Salida del bucle de RabbitMQProcessHandler.") 62 | 63 | def _handle_retry(self, retry_count, max_retries_before_backoff, base_delay, max_delay): 64 | """Maneja la lógica de reintento con backoff exponencial.""" 65 | if self._stop_event.is_set(): 66 | return 67 | 68 | if retry_count <= max_retries_before_backoff: 69 | delay = base_delay 70 | logger.warning(f"Reintento {retry_count}/{max_retries_before_backoff} - esperando {delay}s antes del siguiente intento...") 71 | else: 72 | # Backoff exponencial después de los primeros reintentos 73 | delay = min(base_delay * (2 ** (retry_count - max_retries_before_backoff)), max_delay) 74 | logger.warning(f"Reintento {retry_count} con backoff exponencial - esperando {delay}s antes del siguiente intento...") 75 | 76 | # Esperar con verificación periódica del stop_event 77 | for _ in range(int(delay)): 78 | if self._stop_event.is_set(): 79 | break 80 | time.sleep(1)mon.Configberry import Configberry 81 | from fiscalberry.common.rabbitmq.consumer import RabbitMQConsumer 82 | import pika.exceptions 83 | 84 | logger = logging.getLogger(__name__) 85 | 86 | class RabbitMQProcessHandler: 87 | """Administra el hilo de RabbitMQConsumer: arranque, paro y reintentos.""" 88 | 89 | _instance = None 90 | _lock = threading.Lock() 91 | 92 | def __new__(cls, *args, **kwargs): 93 | with cls._lock: 94 | if not cls._instance: 95 | cls._instance = super().__new__(cls) 96 | cls._instance._initialized = False 97 | return cls._instance 98 | 99 | def __init__(self): 100 | if self._initialized: 101 | return 102 | self._thread = None 103 | self._stop_event = threading.Event() 104 | self.config = Configberry() 105 | self._initialized = True 106 | 107 | 108 | def start(self, message_queue): 109 | """Arranca el consumidor en un hilo daemon (no bloqueante).""" 110 | if self._thread and self._thread.is_alive(): 111 | logger.warning("RabbitMQ thread ya en ejecución.") 112 | return 113 | 114 | host = self.config.get("RabbitMq", "host") 115 | port = self.config.get("RabbitMq", "port") 116 | user = self.config.get("RabbitMq", "user") 117 | password = self.config.get("RabbitMq", "password") 118 | 119 | # TODO deberia usarse esto: queue_name = self.config.get("RabbitMq", "queue") 120 | queue_name = self.config.get("SERVIDOR", "uuid") 121 | self._stop_event.clear() 122 | self._thread = threading.Thread( 123 | target=self._run_consumer, 124 | args=(host, port, user, password, queue_name, message_queue), 125 | daemon=True 126 | ) 127 | self._thread.start() 128 | logger.info("RabbitMQ thread iniciado.") 129 | #self._thread.join() # No bloqueante, solo para iniciar el hilo 130 | #logger.info("RabbitMQ thread en ejecución.") 131 | 132 | 133 | 134 | def _run_consumer(self, host, port, user, password, queue_name, message_queue): 135 | """Loop de conexión/reconexión al servidor RabbitMQ.""" 136 | while not self._stop_event.is_set(): 137 | try: 138 | consumer = RabbitMQConsumer( 139 | host=host, 140 | port=port, 141 | user=user, 142 | password=password, 143 | queue_name=queue_name, 144 | message_queue=message_queue 145 | ) 146 | consumer.start() 147 | # consumer.start() sólo retorna al desconectarse o error. 148 | except Exception as ex: 149 | logger.error(f"Error en RabbitMQConsumer: {ex}", exc_info=True) 150 | if not self._stop_event.is_set(): 151 | logger.warning("RabbitMQConsumer detenido; reintentando en 5 s...") 152 | time.sleep(5) 153 | logger.info("Salida del bucle de RabbitMQProcessHandler.") 154 | 155 | def stop(self, timeout: float = 1): 156 | """Detiene el hilo y espera su finalización.""" 157 | if self._thread and self._thread.is_alive(): 158 | logger.info("Deteniendo RabbitMQConsumer...") 159 | self._stop_event.set() 160 | self._thread.join(timeout) 161 | if self._thread.is_alive(): 162 | logger.warning("RabbitMQConsumer no se detuvo a tiempo.") 163 | else: 164 | logger.info("RabbitMQConsumer detenido correctamente.") 165 | else: 166 | logger.warning("No hay hilo de RabbitMQConsumer en ejecución.") 167 | self._thread = None 168 | self._stop_event.clear() 169 | logger.info("RabbitMQProcessHandler detenido.") 170 | 171 | def configure_and_restart(self, data: dict, message_queue): 172 | """ 173 | Recibe el JSON, actualiza Configberry si cambia y reinicia el hilo. 174 | data debe traer keys 'RabbitMq' y 'Paxaprinter' como en tu interfaz HiDto. 175 | """ 176 | rabbit_cfg = data.get('RabbitMq', {}) 177 | # valida campos... 178 | host = rabbit_cfg.get('host'); port = int(rabbit_cfg.get('port', 0)) 179 | user = rabbit_cfg.get('user'); pwd = rabbit_cfg.get('password') 180 | vhost = rabbit_cfg.get('vhost'); queue = rabbit_cfg.get('queue') 181 | # compara con lo actual 182 | curr = { 183 | "host": self.config.get("RabbitMq", "host"), 184 | "port": self.config.get("RabbitMq", "port"), 185 | "user": self.config.get("RabbitMq", "user"), 186 | "password": self.config.get("RabbitMq", "password"), 187 | "vhost": self.config.get("RabbitMq", "vhost"), 188 | "queue": self.config.get("RabbitMq", "queue"), 189 | } 190 | new = {"host": host, "port": port, "user": user, "password": pwd, "vhost": vhost, "queue": queue} 191 | if curr != new: 192 | self.config.set("RabbitMq", new) 193 | logger.info("Configuración de RabbitMQ actualizada en disk.") 194 | 195 | # compara con lo actual de Paxaprinter 196 | pax_cfg = data.get('Paxaprinter', {}) 197 | alias = pax_cfg.get('alias') 198 | tenant = pax_cfg.get('tenant') 199 | site_name = pax_cfg.get('site_name') 200 | curr_pax = { 201 | "alias": self.config.get("Paxaprinter", "alias"), 202 | "tenant": self.config.get("Paxaprinter", "tenant"), 203 | "site_name": self.config.get("Paxaprinter", "site_name") 204 | } 205 | new_pax = {"alias": alias, "tenant": tenant, "site_name": site_name} 206 | if curr_pax != new_pax: 207 | self.config.set("Paxaprinter", new_pax) 208 | logger.info("Configuración de Paxaprinter actualizada en disk.") 209 | 210 | # guarda en config y reinicia hilo 211 | if self._thread and self._thread.is_alive(): 212 | self.stop(timeout=5) 213 | self.start(message_queue) 214 | 215 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/fiscalberry/common/rabbitmq/process_handler.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import socket 3 | import time 4 | import logging 5 | from fiscalberry.common.Configberry import Configberry 6 | from fiscalberry.common.rabbitmq.consumer import RabbitMQConsumer 7 | import pika.exceptions 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class RabbitMQProcessHandler: 12 | """Administra el hilo de RabbitMQConsumer: arranque, paro y reintentos.""" 13 | 14 | _instance = None 15 | _lock = threading.Lock() 16 | 17 | def __new__(cls, *args, **kwargs): 18 | with cls._lock: 19 | if not cls._instance: 20 | cls._instance = super().__new__(cls) 21 | cls._instance._initialized = False 22 | return cls._instance 23 | 24 | def __init__(self): 25 | if self._initialized: 26 | return 27 | self._thread = None 28 | self._stop_event = threading.Event() 29 | self.config = Configberry() 30 | self._initialized = True 31 | 32 | def start(self, message_queue): 33 | """Arranca el consumidor en un hilo daemon (no bloqueante).""" 34 | if self._thread and self._thread.is_alive(): 35 | logger.warning("RabbitMQ thread ya en ejecución.") 36 | return 37 | 38 | host = self.config.get("RabbitMq", "host") 39 | port = self.config.get("RabbitMq", "port") 40 | user = self.config.get("RabbitMq", "user") 41 | password = self.config.get("RabbitMq", "password") 42 | 43 | # TODO deberia usarse esto: queue_name = self.config.get("RabbitMq", "queue") 44 | queue_name = self.config.get("SERVIDOR", "uuid") 45 | self._stop_event.clear() 46 | self._thread = threading.Thread( 47 | target=self._run_consumer, 48 | args=(host, port, user, password, queue_name, message_queue), 49 | daemon=True 50 | ) 51 | self._thread.start() 52 | logger.info("RabbitMQ thread iniciado.") 53 | 54 | def _check_network_connectivity(self, host, port, timeout=5): 55 | """Verifica la conectividad de red básica antes de intentar conectar con RabbitMQ.""" 56 | try: 57 | # Primero verificar resolución DNS 58 | socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM) 59 | 60 | # Luego verificar si el puerto está abierto 61 | with socket.create_connection((host, port), timeout=timeout): 62 | return True 63 | except socket.gaierror as e: 64 | logger.error(f"Error de resolución DNS para {host}: {e}") 65 | return False 66 | except (socket.timeout, ConnectionRefusedError, OSError) as e: 67 | logger.error(f"No se puede conectar a {host}:{port} - {e}") 68 | return False 69 | except Exception as e: 70 | logger.error(f"Error inesperado verificando conectividad: {e}") 71 | return False 72 | 73 | def _run_consumer(self, host, port, user, password, queue_name, message_queue): 74 | """Loop de conexión/reconexión al servidor RabbitMQ con backoff exponencial.""" 75 | retry_count = 0 76 | max_retries_before_backoff = 3 77 | base_delay = 5 # segundos 78 | max_delay = 300 # 5 minutos máximo 79 | 80 | while not self._stop_event.is_set(): 81 | try: 82 | # Verificar conectividad básica antes de intentar conexión RabbitMQ 83 | if not self._check_network_connectivity(host, int(port)): 84 | retry_count += 1 85 | logger.warning(f"Conectividad de red falló para {host}:{port}") 86 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 87 | continue 88 | 89 | consumer = RabbitMQConsumer( 90 | host=host, 91 | port=port, 92 | user=user, 93 | password=password, 94 | queue_name=queue_name, 95 | message_queue=message_queue 96 | ) 97 | consumer.start() 98 | # Si llegamos aquí, la conexión fue exitosa, resetear contador 99 | retry_count = 0 100 | logger.info("Conexión RabbitMQ establecida exitosamente") 101 | # consumer.start() sólo retorna al desconectarse o error. 102 | except socket.gaierror as ex: 103 | retry_count += 1 104 | logger.error(f"Error de resolución DNS para '{host}': {ex}") 105 | logger.error("Posibles soluciones:") 106 | logger.error("1. Verificar que el hostname 'rabbitmq' esté configurado correctamente") 107 | logger.error("2. Verificar conectividad de red") 108 | logger.error("3. Verificar que el servidor RabbitMQ esté ejecutándose") 109 | logger.error("4. Considerar usar una IP directa en lugar del hostname") 110 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 111 | except pika.exceptions.AMQPConnectionError as ex: 112 | retry_count += 1 113 | logger.error(f"Error de conexión AMQP a {host}:{port}: {ex}") 114 | logger.error("Verificar que el servidor RabbitMQ esté ejecutándose y accesible") 115 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 116 | except pika.exceptions.ProbableAuthenticationError as ex: 117 | retry_count += 1 118 | logger.error(f"Error de autenticación con RabbitMQ: {ex}") 119 | logger.error(f"Verificar credenciales - Usuario: {user}, VHost: {self.config.get('RabbitMq', 'vhost', '/')}") 120 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 121 | except pika.exceptions.ProbableAccessDeniedError as ex: 122 | retry_count += 1 123 | logger.error(f"Acceso denegado a RabbitMQ: {ex}") 124 | logger.error(f"Verificar permisos del usuario '{user}' en vhost '{self.config.get('RabbitMq', 'vhost', '/')}'") 125 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 126 | except ConnectionError as ex: 127 | retry_count += 1 128 | logger.error(f"Error de conexión de red a {host}:{port}: {ex}") 129 | logger.error("Verificar conectividad de red y que el puerto esté abierto") 130 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 131 | except Exception as ex: 132 | retry_count += 1 133 | logger.error(f"Error inesperado en RabbitMQConsumer: {ex}", exc_info=True) 134 | self._handle_retry(retry_count, max_retries_before_backoff, base_delay, max_delay) 135 | 136 | logger.info("Salida del bucle de RabbitMQProcessHandler.") 137 | 138 | def _handle_retry(self, retry_count, max_retries_before_backoff, base_delay, max_delay): 139 | """Maneja la lógica de reintento con backoff exponencial.""" 140 | if self._stop_event.is_set(): 141 | return 142 | 143 | if retry_count <= max_retries_before_backoff: 144 | delay = base_delay 145 | logger.warning(f"Reintento {retry_count}/{max_retries_before_backoff} - esperando {delay}s antes del siguiente intento...") 146 | else: 147 | # Backoff exponencial después de los primeros reintentos 148 | delay = min(base_delay * (2 ** (retry_count - max_retries_before_backoff)), max_delay) 149 | logger.warning(f"Reintento {retry_count} con backoff exponencial - esperando {delay}s antes del siguiente intento...") 150 | 151 | # Esperar con verificación periódica del stop_event 152 | for _ in range(int(delay)): 153 | if self._stop_event.is_set(): 154 | break 155 | time.sleep(1) 156 | 157 | def stop(self, timeout: float = 1): 158 | """Detiene el hilo y espera su finalización.""" 159 | if self._thread and self._thread.is_alive(): 160 | logger.info("Deteniendo RabbitMQConsumer...") 161 | self._stop_event.set() 162 | self._thread.join(timeout) 163 | if self._thread.is_alive(): 164 | logger.warning("RabbitMQConsumer no se detuvo a tiempo.") 165 | else: 166 | logger.info("RabbitMQConsumer detenido correctamente.") 167 | else: 168 | logger.warning("No hay hilo de RabbitMQConsumer en ejecución.") 169 | self._thread = None 170 | self._stop_event.clear() 171 | logger.info("RabbitMQProcessHandler detenido.") 172 | 173 | def configure_and_restart(self, data: dict, message_queue): 174 | """ 175 | Recibe el JSON, actualiza Configberry si cambia y reinicia el hilo. 176 | data debe traer keys 'RabbitMq' y 'Paxaprinter' como en tu interfaz HiDto. 177 | 178 | LÓGICA: Para cada campo, si tiene contenido en config.ini, usar ese valor. 179 | Solo usar valores de SocketIO para completar campos vacíos en config. 180 | """ 181 | rabbit_cfg = data.get('RabbitMq', {}) 182 | 183 | def get_config_or_message_value(config_key, message_key, default_value=""): 184 | """ 185 | Obtiene valor del config.ini si existe y no está vacío, sino del mensaje SocketIO. 186 | Prioridad: config.ini -> mensaje SocketIO -> valor por defecto 187 | """ 188 | config_value = self.config.get("RabbitMq", config_key, fallback="") 189 | if config_value and str(config_value).strip(): 190 | logger.info(f"{config_key}: usando config.ini -> {config_value}") 191 | return config_value 192 | else: 193 | message_value = rabbit_cfg.get(message_key, default_value) 194 | logger.info(f"{config_key}: config.ini vacío, usando SocketIO -> {message_value}") 195 | return message_value 196 | 197 | logger.info("=== Configuración RabbitMQ - Prioridad config.ini ===") 198 | 199 | # Aplicar la lógica a todos los parámetros 200 | host = get_config_or_message_value("host", "host") 201 | port_str = get_config_or_message_value("port", "port", "5672") 202 | user = get_config_or_message_value("user", "user", "guest") 203 | pwd = get_config_or_message_value("password", "password", "guest") 204 | vhost = get_config_or_message_value("vhost", "vhost", "/") 205 | queue = get_config_or_message_value("queue", "queue", "") 206 | 207 | # Convertir puerto a entero 208 | try: 209 | port = int(port_str) 210 | except (ValueError, TypeError): 211 | logger.warning(f"Puerto inválido '{port_str}', usando 5672 por defecto") 212 | port = 5672 213 | 214 | # compara con lo actual 215 | curr = { 216 | "host": self.config.get("RabbitMq", "host"), 217 | "port": self.config.get("RabbitMq", "port"), 218 | "user": self.config.get("RabbitMq", "user"), 219 | "password": self.config.get("RabbitMq", "password"), 220 | "vhost": self.config.get("RabbitMq", "vhost"), 221 | "queue": self.config.get("RabbitMq", "queue"), 222 | } 223 | new = {"host": host, "port": port, "user": user, "password": pwd, "vhost": vhost, "queue": queue} 224 | 225 | # Verificar si hay cambios en la configuración 226 | if curr != new: 227 | self.config.set("RabbitMq", new) 228 | logger.info("Configuración de RabbitMQ actualizada en disk con valores prioritarios del config.ini") 229 | else: 230 | logger.info("Configuración de RabbitMQ sin cambios") 231 | 232 | # compara con lo actual de Paxaprinter 233 | pax_cfg = data.get('Paxaprinter', {}) 234 | alias = pax_cfg.get('alias') 235 | tenant = pax_cfg.get('tenant') 236 | site_name = pax_cfg.get('site_name') 237 | curr_pax = { 238 | "alias": self.config.get("Paxaprinter", "alias"), 239 | "tenant": self.config.get("Paxaprinter", "tenant"), 240 | "site_name": self.config.get("Paxaprinter", "site_name") 241 | } 242 | new_pax = {"alias": alias, "tenant": tenant, "site_name": site_name} 243 | if curr_pax != new_pax: 244 | self.config.set("Paxaprinter", new_pax) 245 | logger.info("Configuración de Paxaprinter actualizada en disk.") 246 | 247 | # guarda en config y reinicia hilo 248 | if self._thread and self._thread.is_alive(): 249 | self.stop(timeout=5) 250 | self.start(message_queue) -------------------------------------------------------------------------------- /src/fiscalberry/ui/kv/main.kv: -------------------------------------------------------------------------------- 1 | ScreenManager: 2 | LoginScreen: 3 | name: "login" 4 | MainScreen: 5 | name: "main" 6 | LogScreen: # Add LogScreen here 7 | name: "logs" # Give it a name for navigation 8 | 9 | : 10 | BoxLayout: 11 | orientation: 'vertical' 12 | padding: 20 13 | spacing: 15 14 | canvas.before: 15 | Color: 16 | rgba: 0.9, 0.9, 1, 1 # Fondo celeste casi blanco 17 | Rectangle: 18 | pos: self.pos 19 | size: self.size 20 | 21 | Image: 22 | source: app.logo_image 23 | size_hint_y: None 24 | height: 96 25 | allow_stretch: True 26 | keep_ratio: True 27 | color: 1, 1, 1, 1 28 | margin_bottom: 10 29 | AsyncImage: 30 | source: root.qrCodeLink 31 | size_hint_y: None 32 | height: 96 33 | allow_stretch: True 34 | keep_ratio: True 35 | color: 1, 1, 1, 1 36 | margin_bottom: 10 37 | Button: 38 | text: "Hace click aquí para vincular el dispositivo" 39 | size_hint_y: None 40 | height: 40 41 | background_color: 0.8, 0.5, 0.2, 1 # Naranja suave 42 | color: 1, 1, 1, 1 43 | on_press: import webbrowser; webbrowser.open(root.adoptarLink) 44 | 45 | : 46 | BoxLayout: 47 | orientation: 'vertical' 48 | padding: 20 49 | spacing: 15 50 | canvas.before: 51 | Color: 52 | rgba: 0.9, 0.9, 1, 1 # Fondo celeste casi blanco 53 | Rectangle: 54 | pos: self.pos 55 | size: self.size 56 | 57 | Image: 58 | source: app.logo_image 59 | size_hint_y: None 60 | height: 96 61 | allow_stretch: True 62 | keep_ratio: True 63 | color: 1, 1, 1, 1 64 | margin_bottom: 10 65 | 66 | 67 | Label: 68 | text: "Iniciar Sesión" 69 | font_size: 24 70 | color: 0.1, 0.2, 0.5, 1 # Azul oscuro 71 | 72 | Label: 73 | text: "Utilice su usuario y contraseña para acceder al sistema." 74 | size_hint_y: None 75 | height: 30 76 | halign: "center" 77 | valign: "middle" 78 | text_size: self.size 79 | color: 0.5, 0.5, 0.5, 1 # Gris tenue 80 | 81 | Label: 82 | text: root.errorMsg 83 | color: 0.8, 0.2, 0.2, 1 # Rojo estándar para errores 84 | font_size: 16 85 | size_hint_y: None 86 | height: 30 87 | halign: "center" 88 | valign: "middle" 89 | text_size: self.size 90 | 91 | TextInput: 92 | id: username 93 | hint_text: "Usuario" 94 | multiline: False 95 | size_hint_y: None 96 | height: 50 97 | background_color: 1, 1, 1, 1 98 | foreground_color: 0, 0, 0, 1 99 | padding: [10, 10] 100 | on_text_validate: password.focus = True # Al presionar Enter, enfocar el siguiente input 101 | TextInput: 102 | id: password 103 | hint_text: "Contraseña" 104 | password: True 105 | multiline: False 106 | size_hint_y: None 107 | height: 50 108 | background_color: 1, 1, 1, 1 109 | foreground_color: 0, 0, 0, 1 110 | padding: [10, 10] 111 | on_text_validate: root.login(username.text, password.text) # Al presionar Enter, ejecutar login 112 | Button: 113 | text: "Login" 114 | size_hint_y: None 115 | height: 50 116 | background_color: 0.1, 0.2, 0.5, 1 # Azul oscuro 117 | color: 1, 1, 1, 1 118 | on_press: root.login(username.text, password.text) 119 | 120 | # Define a reusable Separator widget 121 | : 122 | canvas: 123 | Color: 124 | rgba: 0.8, 0.8, 0.8, 1 # Light gray color for separator 125 | Rectangle: 126 | pos: self.x, self.center_y - dp(0.5) # Center the line vertically 127 | size: self.width, dp(1) # Make it 1dp thick 128 | 129 | : 130 | BoxLayout: 131 | orientation: 'vertical' 132 | padding: dp(20) 133 | spacing: dp(15) 134 | canvas.before: 135 | Color: 136 | rgba: 0.95, 0.95, 1, 1 # Slightly lighter blue background 137 | Rectangle: 138 | pos: self.pos 139 | size: self.size 140 | 141 | # Header: Logo, Title, Overall Status Icon 142 | BoxLayout: 143 | orientation: 'horizontal' 144 | size_hint_y: None 145 | height: dp(48) # Fixed height for the header 146 | spacing: dp(10) 147 | Image: 148 | source: app.logo_image 149 | size_hint: None, None # Use explicit size 150 | size: dp(48), dp(48) 151 | allow_stretch: True 152 | keep_ratio: True 153 | Label: 154 | text: app.name # Consider using app.name if available 155 | font_size: '20sp' 156 | halign: 'center' 157 | valign: 'middle' 158 | color: 0.1, 0.2, 0.5, 1 # Dark blue text 159 | size_hint_x: 1 # Allow label to expand horizontally 160 | Image: 161 | # Show overall status based on cloud connection primarily 162 | source: root.connected_image if app.sioConnected else root.disconnected_image 163 | size_hint: None, None # Use explicit size 164 | size: dp(32), dp(32) 165 | 166 | Separator: 167 | height: dp(1) # Use the defined Separator 168 | 169 | # Status Information Area 170 | BoxLayout: 171 | orientation: 'vertical' 172 | size_hint_y: None # Size based on content 173 | height: self.minimum_height # Calculate height automatically 174 | spacing: dp(10) 175 | 176 | Label: 177 | text: "Estado de Conexión" 178 | font_size: '18sp' 179 | color: 0.1, 0.2, 0.5, 1 180 | size_hint_y: None 181 | height: self.texture_size[1] # Fit height to text 182 | 183 | # Cloud Status (SocketIO) 184 | BoxLayout: 185 | orientation: 'horizontal' 186 | size_hint_y: None 187 | height: dp(24) # Fixed height for status line 188 | spacing: dp(5) 189 | Image: 190 | source: root.connected_image if app.sioConnected else root.disconnected_image 191 | size_hint: None, None 192 | size: dp(24), dp(24) 193 | Label: 194 | # Display text and color based on connection status 195 | text: "Nube (Sync): " + ("Conectado" if app.sioConnected else "Desconectado") 196 | color: (0.2, 0.8, 0.2, 1) if app.sioConnected else (0.8, 0.2, 0.2, 1) # Green/Red 197 | halign: 'left' 198 | valign: 'middle' 199 | text_size: self.width, None # Enable text wrapping if needed 200 | size_hint_x: 1 201 | 202 | # Queue Status (RabbitMQ) 203 | BoxLayout: 204 | orientation: 'horizontal' 205 | size_hint_y: None 206 | height: dp(24) # Fixed height for status line 207 | spacing: dp(5) 208 | Image: 209 | # Assumes root has appropriate images for connection status 210 | source: root.connected_image if app.rabbitMqConnected else root.disconnected_image 211 | size_hint: None, None 212 | size: dp(24), dp(24) 213 | Label: 214 | # Display text and color based on connection status 215 | text: "Cola Local (Cmds): " + ("Conectado" if app.rabbitMqConnected else "Desconectado") 216 | color: (0.2, 0.8, 0.2, 1) if app.rabbitMqConnected else (0.8, 0.2, 0.2, 1) # Green/Red 217 | halign: 'left' 218 | valign: 'middle' 219 | text_size: self.width, None # Enable text wrapping 220 | size_hint_x: 1 221 | 222 | Separator: 223 | height: dp(1) 224 | 225 | # Site Information Area 226 | BoxLayout: 227 | orientation: 'vertical' 228 | size_hint_y: None # Size based on content 229 | height: self.minimum_height # Calculate height automatically 230 | spacing: dp(5) 231 | 232 | Label: 233 | text: "Información del Sitio" 234 | font_size: '18sp' 235 | color: 0.1, 0.2, 0.5, 1 236 | size_hint_y: None 237 | height: self.texture_size[1] 238 | 239 | Label: 240 | text: f"Comercio: {app.siteName}" 241 | font_size: '14sp' 242 | color: 0.3, 0.3, 0.3, 1 # Dark gray text 243 | halign: 'left' 244 | valign: 'middle' 245 | text_size: self.width, None # Enable wrapping 246 | size_hint_y: None 247 | height: self.texture_size[1] # Fit height to text 248 | 249 | Label: 250 | text: f"Dispositivo: {app.siteAlias}" 251 | font_size: '14sp' 252 | color: 0.3, 0.3, 0.3, 1 253 | halign: 'left' 254 | valign: 'middle' 255 | text_size: self.width, None 256 | size_hint_y: None 257 | height: self.texture_size[1] 258 | 259 | # Spacer widget to push subsequent elements to the bottom 260 | Widget: 261 | 262 | # Action Button 263 | Button: 264 | text: "Ver Logs del Servicio" 265 | size_hint_y: None # Fixed height 266 | height: dp(50) 267 | background_color: 0.1, 0.2, 0.5, 1 # Consistent button blue 268 | color: 1, 1, 1, 1 # White text 269 | on_press: app.root.current = "logs" # Navigate to LogScreen 270 | 271 | # Footer Information (UUID, Version, Host) 272 | BoxLayout: 273 | orientation: 'horizontal' 274 | size_hint_y: None 275 | height: dp(20) # Compact footer height 276 | spacing: dp(10) 277 | Label: 278 | text: f"UUID: {app.uuid[:8]}" 279 | font_size: '10sp' # Smaller font for footer details 280 | color: 0.5, 0.5, 0.5, 1 # Gray text 281 | halign: "left" 282 | valign: "middle" 283 | size_hint_x: 0.4 # Allocate space 284 | Label: 285 | text: f"v{app.version}" 286 | font_size: '10sp' 287 | color: 0.5, 0.5, 0.5, 1 288 | halign: "center" 289 | valign: "middle" 290 | size_hint_x: 0.2 291 | Label: 292 | text: f"Host: {app.host}" 293 | font_size: '10sp' 294 | color: 0.5, 0.5, 0.5, 1 295 | halign: "right" 296 | valign: "middle" 297 | size_hint_x: 0.4 298 | 299 | : 300 | BoxLayout: 301 | orientation: 'vertical' 302 | padding: 10 303 | spacing: 10 304 | 305 | Label: 306 | text: "Logs del Servicio" 307 | font_size: 24 308 | size_hint_y: None 309 | height: 50 310 | 311 | Button: 312 | text: root.logFilePath 313 | font_size: 12 314 | size_hint_y: None 315 | height: self.texture_size[1] + dp(10) # Adjust height based on text 316 | # Style to look like a clickable link 317 | background_color: 0, 0, 0, 0 # Transparent background 318 | color: 0.1, 0.2, 0.8, 1 # Blue color like a hyperlink 319 | # Action to open the file using a method defined in the Python class 320 | # Ensure 'open_log_file' method exists in your LogScreen Python class 321 | on_press: root.open_log_file() 322 | 323 | ScrollView: 324 | id: scroll_view # Agregar un ID para acceder desde Python 325 | size_hint: (1, 1) 326 | TextInput: 327 | id: log_output 328 | text: root.logs 329 | readonly: True 330 | font_size: 14 331 | background_color: 0, 0, 0, 1 # Fondo negro 332 | foreground_color: 1, 1, 1, 1 # Texto blanco 333 | size_hint_y: None 334 | height: self.minimum_height 335 | multiline: True 336 | 337 | Button: 338 | text: "Volver" 339 | size_hint_y: None 340 | height: 50 341 | on_press: app.root.current = "main" -------------------------------------------------------------------------------- /src/fiscalberry/common/service_controller.py: -------------------------------------------------------------------------------- 1 | from fiscalberry.common.fiscalberry_logger import getLogger 2 | import sys 3 | import os 4 | import signal 5 | import socketio 6 | from fiscalberry.common.fiscalberry_sio import FiscalberrySio 7 | from fiscalberry.common.discover import send_discover_in_thread 8 | from fiscalberry.common.Configberry import Configberry 9 | import time 10 | import threading 11 | from fiscalberry.common.fiscalberry_logger import getLogger 12 | logger = getLogger() 13 | 14 | class ServiceController: 15 | """ 16 | Controla el inicio y detención del servicio socketio de Fiscalberry. 17 | El servicio controla 2 cosas: por un lado el socketio queda como daemond, el discover se ejecuta y finaliza. 18 | SocketIO sirve para mensajear y recibir mensajes de la aplicación cliente. Sirve para comunicarse y enviarle comandos de managment bidireccionales. 19 | Por ejemplo un event importante que tiene el servicio de SocketIO es inicializar el cliente de RabbitMQ (RabbitMQProcessHandler) 20 | El discover: es un request donde envia informacion como la IP, el UUID y datos de las impresoras instaladas en esta PC. Envia el request y el thread deberia finalizar. 21 | 22 | El servicio de socketio queda corriendo en segundo plano y se encarga de recibir mensajes y enviar respuestas. 23 | """ 24 | 25 | sio: FiscalberrySio = None 26 | _instance = None 27 | 28 | def __new__(cls): 29 | if cls._instance is None: 30 | cls._instance = super(ServiceController, cls).__new__(cls) 31 | return cls._instance 32 | 33 | def __init__(self): 34 | if hasattr(self, 'initialized'): 35 | return 36 | 37 | self.initialized = True 38 | self.socketio_thread = None 39 | self.discover_thread = None 40 | self.configberry = Configberry() 41 | self._stop_event = threading.Event() # Evento para detener limpiamente 42 | self.initial_retries = 0 # Contador para chequeo inicial de UUID 43 | 44 | sio_host = self.configberry.get("SERVIDOR", "sio_host") 45 | if not sio_host: 46 | logger.error("sio_host no configurado. Abortando.") 47 | os._exit(1) 48 | 49 | # --- Bucle de chequeo inicial de UUID --- 50 | uuidval = self.configberry.get("SERVIDOR", "uuid", fallback="") 51 | if not uuidval: 52 | logger.error(f"UUID NO encontrado en config file: {uuidval}") 53 | os._exit(1) 54 | 55 | self.sio = FiscalberrySio(sio_host, uuidval) 56 | 57 | # Configurar manejo de señales 58 | self._setup_signal_handlers() 59 | 60 | def _setup_signal_handlers(self): 61 | """Configura los manejadores de señales para detener limpiamente.""" 62 | # Guardar las referencias a los manejadores originales 63 | self._original_sigint_handler = signal.getsignal(signal.SIGINT) 64 | self._original_sigterm_handler = signal.getsignal(signal.SIGTERM) 65 | 66 | # Registrar nuestros manejadores 67 | signal.signal(signal.SIGINT, self._signal_handler) 68 | signal.signal(signal.SIGTERM, self._signal_handler) 69 | 70 | def _signal_handler(self, sig, frame): 71 | """Manejador de señales para cierre graceful.""" 72 | logger.info(f"Recibida señal {signal.Signals(sig).name}. Deteniendo servicios...") 73 | 74 | # Solicitar detención limpia (sin sys.exit) 75 | self._stop_services_only() 76 | 77 | # Terminar inmediatamente después de la limpieza 78 | logger.info("Terminando aplicación...") 79 | os._exit(0) # Terminar de forma inmediata y definitiva 80 | 81 | def is_service_running(self): 82 | """Verifica si el servicio ya está en ejecución.""" 83 | # Verifica si el hilo está activo 84 | if self.socketio_thread and self.socketio_thread.is_alive(): 85 | return True 86 | return False 87 | 88 | def isSocketIORunning(self): 89 | """ 90 | Verifica si el hilo de SIO está en ejecución. 91 | """ 92 | return self.sio.isSioRunning() 93 | 94 | def isRabbitRunning(self): 95 | """ 96 | Verifica si el hilo de RabbitMQ está en ejecución. 97 | """ 98 | return self.sio.isRabbitMQRunning() 99 | 100 | def toggle_service(self): 101 | """Alterna el estado del servicio.""" 102 | if self.is_service_running(): 103 | logger.info("Deteniendo el servicio...") 104 | return self.stop() 105 | else: 106 | logger.info("Iniciando el servicio...") 107 | return self.start() 108 | 109 | def _run_sio_instance(self): 110 | """Ejecuta una instancia/conexión de FiscalberrySio y ESPERA a que termine.""" 111 | sio_internal_thread = None # Para almacenar el hilo devuelto por sio.start() 112 | try: 113 | # sio.start() crea y arranca el hilo interno (_run) y lo devuelve 114 | sio_internal_thread = self.sio.start() 115 | 116 | # ¡IMPORTANTE! Esperar a que el hilo interno de SIO termine. 117 | if sio_internal_thread and isinstance(sio_internal_thread, threading.Thread): 118 | # Usar join con timeout para poder verificar periódicamente si hay una solicitud de stop 119 | while sio_internal_thread.is_alive() and not self._stop_event.is_set(): 120 | sio_internal_thread.join(timeout=1.0) 121 | 122 | if self._stop_event.is_set() and sio_internal_thread.is_alive(): 123 | logger.info("Stop event detected during SIO internal thread join") 124 | else: 125 | logger.info("FiscalberrySio internal thread finished.") 126 | else: 127 | logger.warning("self.start() did not return a running thread to wait for.") 128 | 129 | except socketio.exceptions.ConnectionError as e: 130 | logger.error(f"SIO Connection Error in instance thread: {e}", exc_info=False) 131 | except Exception as e: 132 | logger.error(f"Unhandled Exception in SIO instance thread: {e}", exc_info=True) 133 | finally: 134 | logger.info("Exiting _run_sio_instance.") 135 | 136 | def start(self): 137 | """Inicia y mantiene vivo el proceso de conexión SIO.""" 138 | logger.info("Iniciando Fiscalberry SIO Service Loop") 139 | self._stop_event.clear() 140 | self.initial_retries = 0 141 | 142 | if self._stop_event.is_set(): 143 | logger.info("Stop requested during initial UUID check.") 144 | return # Salir si se pidió detener 145 | 146 | # Enviar el discover al servidor la primera vez 147 | self.discover_thread = send_discover_in_thread() 148 | self.discover_thread.start() 149 | 150 | # --- Bucle principal de reconexión --- 151 | while not self._stop_event.is_set(): 152 | # Creamos un hilo que ejecutará _run_sio_instance 153 | self.socketio_thread = threading.Thread( 154 | target=self._run_sio_instance, 155 | daemon=True # Daemon para que no bloquee la salida si el principal muere 156 | ) 157 | logger.info("* * * * * SocketIO thread start.") 158 | self.socketio_thread.start() 159 | 160 | # En lugar de un join simple que puede bloquear indefinidamente, 161 | # usamos un join con timeout para verificar periódicamente el estado 162 | while self.socketio_thread.is_alive() and not self._stop_event.is_set(): 163 | self.socketio_thread.join(timeout=1.0) 164 | 165 | logger.info("* * * * * SocketIO thread finished or stop requested.") 166 | 167 | # Si el hilo terminó, verificamos si fue por una señal de stop 168 | if self._stop_event.is_set(): 169 | logger.info("Stop event received. Exiting SIO loop.") 170 | break # Salir del bucle while 171 | 172 | # Si no fue por stop, asumimos desconexión/error y reintentamos 173 | logger.warning("SIO thread terminated. Reconnecting in 5 seconds...") 174 | time.sleep(5) 175 | 176 | logger.info("Fiscalberry SIO Service Loop finished.") 177 | 178 | def _is_gui_mode(self): 179 | """Detecta si la aplicación está ejecutándose en modo GUI.""" 180 | try: 181 | # Intentar importar kivy para verificar si está disponible 182 | import kivy 183 | from kivy.app import App 184 | # Si hay una app de Kivy ejecutándose, estamos en modo GUI 185 | return App.get_running_app() is not None 186 | except ImportError: 187 | # Si no se puede importar Kivy, definitivamente es CLI 188 | return False 189 | except: 190 | # Si hay cualquier otro error, asumir CLI 191 | return False 192 | 193 | def stop(self): 194 | """Detiene el bucle de servicios y la instancia SIO.""" 195 | # Detectar automáticamente si estamos en modo GUI o CLI 196 | if self._is_gui_mode(): 197 | return self.stop_for_gui() 198 | else: 199 | return self.stop_for_cli() 200 | 201 | def stop_for_cli(self): 202 | """Detiene el bucle de servicios específicamente para modo CLI.""" 203 | logger.info("# # # Requesting SIO services stop (CLI mode)...") 204 | self._stop_event.set() 205 | 206 | self.sio.stop() 207 | 208 | if self.socketio_thread and self.socketio_thread.is_alive(): 209 | # Dar tiempo para que termine limpiamente 210 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo 211 | if not self.socketio_thread.is_alive(): 212 | break 213 | time.sleep(1) 214 | 215 | if self.socketio_thread.is_alive(): 216 | logger.warning("SIO thread did not stop within the timeout period.") 217 | else: 218 | logger.info("SIO thread already stopped or not started.") 219 | 220 | # Detener el hilo de discover si está activo 221 | if self.discover_thread and self.discover_thread.is_alive(): 222 | logger.info("Stopping discover thread...") 223 | self.discover_thread.join(timeout=2) 224 | if self.discover_thread.is_alive(): 225 | logger.warning("Discover thread did not stop within the timeout period.") 226 | else: 227 | logger.info("Discover thread already stopped or not started.") 228 | 229 | logger.info("SIO services stopped (CLI mode).") 230 | 231 | # Para CLI usamos sys.exit(0) 232 | sys.exit(0) 233 | 234 | return True # Esta línea ya no se ejecutará, pero la dejamos por compatibilidad 235 | 236 | def stop_for_gui(self): 237 | """Detiene el bucle de servicios específicamente para modo GUI.""" 238 | logger.info("# # # Requesting SIO services stop (GUI mode)...") 239 | self._stop_event.set() 240 | 241 | self.sio.stop() 242 | 243 | if self.socketio_thread and self.socketio_thread.is_alive(): 244 | # Dar tiempo para que termine limpiamente 245 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo 246 | if not self.socketio_thread.is_alive(): 247 | break 248 | time.sleep(1) 249 | 250 | if self.socketio_thread.is_alive(): 251 | logger.warning("SIO thread did not stop within the timeout period.") 252 | else: 253 | logger.info("SIO thread already stopped or not started.") 254 | 255 | # Detener el hilo de discover si está activo 256 | if self.discover_thread and self.discover_thread.is_alive(): 257 | logger.info("Stopping discover thread...") 258 | self.discover_thread.join(timeout=2) 259 | if self.discover_thread.is_alive(): 260 | logger.warning("Discover thread did not stop within the timeout period.") 261 | else: 262 | logger.info("Discover thread already stopped or not started.") 263 | 264 | logger.info("SIO services stopped (GUI mode).") 265 | 266 | # Para GUI NO intentar cerrar Kivy desde aquí - evitar recursión 267 | # La aplicación GUI debe manejar su propio cierre desde on_stop() 268 | return True 269 | 270 | def _stop_services_only(self): 271 | """Detiene solo los servicios sin llamar a sys.exit() - para uso interno.""" 272 | logger.info("# # # Requesting SIO services stop...") 273 | self._stop_event.set() 274 | 275 | self.sio.stop() 276 | 277 | if self.socketio_thread and self.socketio_thread.is_alive(): 278 | # Dar tiempo para que termine limpiamente 279 | for _ in range(5): # Esperar hasta 5 segundos en incrementos de 1 segundo 280 | if not self.socketio_thread.is_alive(): 281 | break 282 | time.sleep(1) 283 | 284 | if self.socketio_thread.is_alive(): 285 | logger.warning("SIO thread did not stop within the timeout period.") 286 | else: 287 | logger.info("SIO thread already stopped or not started.") 288 | 289 | # Detener el hilo de discover si está activo 290 | if self.discover_thread and self.discover_thread.is_alive(): 291 | logger.info("Stopping discover thread...") 292 | self.discover_thread.join(timeout=2) 293 | if self.discover_thread.is_alive(): 294 | logger.warning("Discover thread did not stop within the timeout period.") 295 | else: 296 | logger.info("Discover thread already stopped or not started.") 297 | 298 | logger.info("SIO services stopped.") 299 | 300 | return True 301 | 302 | 303 | 304 | --------------------------------------------------------------------------------