diff --git a/README.md b/README.md index e69de29..15d2b0a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,35 @@ +# Multimedijski krmilnik FRI + +> *Also known as **PolžProjekt** or **KatjaKontroler*** + +## Arhitektura + +Sistem sestavljata dva kartična računalnika (trenutno Raspberry Pi 4) - *krmilnik* in *zaslon*. Na krmilniku je zagnan MQTT broker (mosquitto) in več *driverjev*, ki služijo kot mostovi med napravami v predavalnici in MQTT. Na zaslonu teče spletni brskalnik (chromium) z odprtim *frontendom* (VueJS aplikacija, servirana s krmilnika). Frontend komunicira neposredno z MQTT brokerjem preko WebSockets povezave. + +### Driverji + +- `tse_serial` - relejno polje *TSE ___*, ki krmili napajanje različnih komponent predavalnice, projekcijska platna in senčila +- `barco_telnet` - en projektor tipa *Barco G62* preko novega "Barco Telnet" protokola. Za več projektorjev je potrebnih več instanc driverja +- `projector_motors` - dvigala za projektorje preko dveh modulov *Grove I2C 4-channel SPDT relay* +- `lucke` - razsvetljava v predavalnici preko A-rosso sistema WebSCADA +- `extron_audio_matrix` - avdio matrika - **NI DOKONČAN** +- `barco_rlmw_{http,tcp}` - en projektor serije *Barco RLM W*. Obstajata dve različici (http in tcp) + +## Namestitev + +Namestitev poteka z orodjem *Ansible*, potrebna pa je tudi lokalna namestitev orodja `npm` (za izgradnjo frontenda). V mapi `ansible_deploy` sta ločeni mapi z playbooki za krmilnik in zaslon. + +Osnovni operacijski sistem za krmilnik je *RaspberryPi OS Lite* za zaslon pa *RaspberryPi OS Full*. Namestite ga lahko z orodjem *Raspberry Pi Imager* - pri tem bodite pozorni da nastavite uporabniško ime `pi` in geslo, ki je shranjeno v RC. + +Za namestitev mora imeti Raspberry Pi dostop do interneta, zato ga priključite v omrežje z DHCP. Njegov IP naslov se izpiše na HDMI izhodu, lahko ga pa najdete tudi na drug način. Ker se po prvem zagonu na vgrajeni mrežni kartici konfigurira statičen IP naslov, je priporočeno, da se za namestitev uporablja USB Ethernet adapter. + +Primer namestitve krmilnika za predavalnico P01 (ime v inventory datoteki je `p01_controller`), ki ima trenutno IP naslov `10.32.50.123` (ker se razlikuje od inventory datoteke ga je treba določiti z `-e ansible_host=`): + +```sh +cd ansible_deploy/controller +ansible-playbook playbook.yml -i inventory.yml -l p01_controller -e ansible_host=10.32.50.123 +``` + +## Posodobitev + +Posodobitev poteka enako kot namestitev. Če se izvaja v predavalnici je potrebo poskrbeti, da imajo ciljne naprave dostop do interneta. Za to ni dobrega načina, zato je priporočen enak pristop kot pri namestitvi (USB Ethernet adapter in omrežje z DHCP - lahko celo deljena povezava s prenosnika). diff --git a/ansible_deploy/barcoinv.yml b/ansible_deploy/barcoinv.yml deleted file mode 100644 index 7d8f910..0000000 --- a/ansible_deploy/barcoinv.yml +++ /dev/null @@ -1,6 +0,0 @@ -all: - hosts: - mmctrl-p1: - projectors: - - projector-p1-center - - projector-p1-side diff --git a/ansible_deploy/service.j2 b/ansible_deploy/controller/controller_script.service.j2 similarity index 68% rename from ansible_deploy/service.j2 rename to ansible_deploy/controller/controller_script.service.j2 index b232bbc..b75861a 100644 --- a/ansible_deploy/service.j2 +++ b/ansible_deploy/controller/controller_script.service.j2 @@ -1,6 +1,7 @@ [Unit] Description={{ script_file }} After=multi-user.target +PartOf=mm-controller.target [Service] ExecStart=/usr/local/bin/poetry run python3 -u {{ script_file }} @@ -9,8 +10,7 @@ Restart=always User=pi Group=pi RestartSec=10 -WorkingDirectory=/home/pi/pyServices +WorkingDirectory={{ INSTALL_BASE }} [Install] -WantedBy=multi-user.target -DefaultInstance=main \ No newline at end of file +WantedBy=mm-controller.target diff --git a/ansible_deploy/controller/inventory.yml b/ansible_deploy/controller/inventory.yml new file mode 100644 index 0000000..77b9d43 --- /dev/null +++ b/ansible_deploy/controller/inventory.yml @@ -0,0 +1,82 @@ +predavalnice_pi: + + # Skupne spremenljivke za vse krmilnike + vars: + + ansible_user: pi + + # Multimedia network + static_ip_cidr: "24" + static_ip_gateway: "192.168.192.1" + static_ip_dns_servers: + - "212.235.188.28" + - "212.235.188.29" + + # Lokalni MQTT + mqtt_ip: localhost + mqtt_port: 1883 + + # Nastavitve za vse posamezne predavalnice + hosts: + + p01_controller: + room: P01 + ansible_host: 192.168.192.42 + static_ip: 192.168.192.42 + + # Novi barco projektorji + barco_G62: + - position: main + model: barco_G62 + port: 3023 + ip: 192.168.192.12 + - position: side + model: barco_G62 + port: 3023 + ip: 192.168.192.13 + + tse_box: + + projector_motors: + - position: main + i2c_address: 0x42 + offset: 0 #for when using single 8 channel relay board + - position: side + i2c_address: 0x43 + offset: 4 + + lucke: + url: http://192.168.190.90:8091 + roomId: 1 + bearer_token: 0954afe1-4111-4f89-a123-fea08a55dc46 + + p22_controller: + room: P22 + ansible_host: 192.168.192.43 + static_ip: 192.168.192.43 + + # Novi barco projektorji + barco_G62: + - position: main + model: barco_G62 + port: 3023 + ip: 192.168.192.22 + - position: side + model: barco_G62 + port: 3023 + ip: 192.168.192.23 + + tse_box: + + projector_motors: + - position: main + i2c_address: 0x42 + offset: 0 #for when using single 8 channel relay board + - position: side + i2c_address: 0x43 + offset: 4 + + lucke: + url: http://192.168.190.90:8091 + roomId: 2 + bearer_token: b44c8cdc-d848-4b49-9deb-79023a60a62a diff --git a/ansible_deploy/conf.j2 b/ansible_deploy/controller/malinaConfig.toml.j2 similarity index 85% rename from ansible_deploy/conf.j2 rename to ansible_deploy/controller/malinaConfig.toml.j2 index 56a9b80..61ab7fb 100644 --- a/ansible_deploy/conf.j2 +++ b/ansible_deploy/controller/malinaConfig.toml.j2 @@ -1,7 +1,7 @@ [global] room = "{{ room }}" -mqttIp = "{{ mqtt_ip }}" -mqttPort = "{{ mqtt_port }}" +mqttHost = "{{ mqtt_ip }}" +mqttPort = {{ mqtt_port }} {% if barco_G62 is defined %} {%+ for projector in barco_G62 +%} @@ -12,7 +12,7 @@ port = {{projector.port}} {% endif %} {%+ if tse_box is defined +%} -serial_device = "{{ tse_box.serial_device }}" +{# serial_device = "{{ tse_box.serial_device }}" #} {% endif %} {%+ if projector_motors is defined +%} {# change to appropriate thingy for running them #} @@ -35,4 +35,4 @@ i2c_address = {{motor.i2c_address}} url = "{{ lucke.url }}" roomId = "{{ lucke.roomId }}" bearer_token = "{{ lucke.bearer_token }}" -{% endif %} \ No newline at end of file +{% endif %} diff --git a/ansible_deploy/controller/mm-controller.target.j2 b/ansible_deploy/controller/mm-controller.target.j2 new file mode 100644 index 0000000..3947d55 --- /dev/null +++ b/ansible_deploy/controller/mm-controller.target.j2 @@ -0,0 +1,3 @@ +[Unit] +Description=Krmilnik multimedijskega sistema +After=multi-user.target diff --git a/mosquitto.conf b/ansible_deploy/controller/mosquitto.conf similarity index 99% rename from mosquitto.conf rename to ansible_deploy/controller/mosquitto.conf index 154bbf1..11a6d18 100644 --- a/mosquitto.conf +++ b/ansible_deploy/controller/mosquitto.conf @@ -472,7 +472,7 @@ allow_anonymous true # Note that if the broker is running as a Windows service it will default to # "log_dest none" and neither stdout nor stderr logging is available. # Use "log_dest none" if you wish to disable logging. -#log_dest stderr +log_dest syslog # Types of messages to log. Use multiple log_type lines for logging # multiple types of messages. diff --git a/ansible_deploy/controller/playbook.yml b/ansible_deploy/controller/playbook.yml new file mode 100644 index 0000000..83a9080 --- /dev/null +++ b/ansible_deploy/controller/playbook.yml @@ -0,0 +1,236 @@ +- name: Test playbook + vars: + PROJECT_BASE: "{{playbook_dir}}/../../" + INSTALL_BASE: "/home/pi/pyServices" + + hosts: + - p01_controller + - p22_controller + + handlers: + - name: restart NetworkManager + ansible.builtin.service: + name: NetworkManager + state: restarted + - name: Restart mosquitto + become: true + ansible.builtin.systemd_service: + name: mosquitto.service + state: restarted + + tasks: + + # + # NETWORK CONFIGURATION + # + + - name: set eth0 static IP + become: true + community.general.nmcli: + conn_name: "Multimedia network" + ifname: eth0 + type: ethernet + ip4: "{{ static_ip }}/{{ static_ip_cidr }}" + # Sorry timi + method6: disabled + # Multimedia net doesn't have Internet access, so this iface shouldn't be used for Internet access + never_default4: true + routes4_extended: + - ip: 192.168.0.0/16 + next_hop: "{{ static_ip_gateway }}" + metric: 9999 + - ip: 10.0.0.0/8 + next_hop: "{{ static_ip_gateway }}" + metric: 9999 + # gw4: "{{ static_ip_gateway }}" + state: present + + + # + # SYSTEM DEPENDENCIES + # + - name: Install pkgs + become: true + apt: + name: + - python3-pip + - mosquitto + - nginx + state: latest + update_cache: true + + - name: Install Poetry + become: true + pip: + break_system_packages: true + name: + - poetry + # + # MOSQUITTO + # + - name: mosquitto enable + become: true + ansible.builtin.systemd_service: + name: mosquitto.service + enabled: true + + - name: Copy mosqitconfig + become: true + ansible.builtin.copy: + src: mosquitto.conf + dest: /etc/mosquitto/mosquitto.conf + owner: root + group: root + mode: '0644' + backup: yes + notify: Restart mosquitto + + # + # INSTALL CONTROLLER + # + + - name: Create installation directory + file: + path: "{{INSTALL_BASE}}" + state: directory + + - name: Install controller scripts + ansible.posix.synchronize: + src: "{{PROJECT_BASE}}/controller/" + dest: "{{INSTALL_BASE}}/" + delete: true + archive: false + recursive: true + + - name: template config.toml + ansible.builtin.template: + src: ./malinaConfig.toml.j2 + dest: "{{INSTALL_BASE}}/malinaConfig.toml" + + - name: Install python libraries + ansible.builtin.shell: + cmd: "poetry install" + chdir: "{{INSTALL_BASE}}" + + + # + # INSTALL FRONTEND + # + + - name: Build frontend (localhost) + delegate_to: localhost + ansible.builtin.shell: + cmd: "npm install --dev && npm run build" + chdir: "{{PROJECT_BASE}}/frontend" + + - name: Install frontend + become: true + ansible.posix.synchronize: + src: "{{PROJECT_BASE}}/frontend/dist/" + dest: "/var/www/html/" + delete: true + archive: false + recursive: true + + - name: Fix www root permission + become: true + ansible.builtin.file: + path: "/var/www/html" + owner: pi + group: pi + recurse: true + mode: 'u=rwX,g=rX,o=rX' + + # + # SERVICES + # + + - name: template target + become: true + ansible.builtin.template: + src: ./mm-controller.target.j2 + dest: /etc/systemd/system/mm-controller.target + + - name: Generate systemd services + become: true + block: + # Barco G62 (novi projektor) + - name: Barco G62 services + when: barco_G62 is defined + block: + - name: template service + become: true + vars: + script_file: "{{INSTALL_BASE}}/barco_telnet/barco_G62_control.py %i" + ansible.builtin.template: + src: ./controller_script.service.j2 + dest: /etc/systemd/system/barco@.service + - name: enable service + ansible.builtin.systemd_service: + name: "{{item}}" + enabled: true + state: restarted + daemon_reload: true + loop: + - barco@main.service + - barco@side.service + + # Lifti za projektorje (naši releji) + - name: template projector motors service + when: projector_motors is defined + block: + - name: template service + become: true + vars: + script_file: "{{INSTALL_BASE}}/projector_motors/projector_motors.py" + ansible.builtin.template: + src: ./controller_script.service.j2 + dest: /etc/systemd/system/projector_motors.service + - name: enable service + ansible.builtin.systemd_service: + name: projector_motors.service + enabled: true + state: restarted + daemon_reload: true + + # Power, platna, etc. (TSE relay box) + - name: template tse serial box service + when: tse_box is defined + block: + - name: template service + become: true + vars: + script_file: "{{INSTALL_BASE}}/tse_serial/tse_serial_controler.py" + ansible.builtin.template: + src: ./controller_script.service.j2 + dest: /etc/systemd/system/tse_box.service + - name: enable service + ansible.builtin.systemd_service: + name: tse_box.service + enabled: true + state: restarted + daemon_reload: true + + # a-rosso lučke kontroler + - name: template lucke service + when: lucke is defined + block: + - name: tmeplate service + become: true + vars: + script_file: "{{INSTALL_BASE}}/lucke/luckeControl.py" + ansible.builtin.template: + src: ./controller_script.service.j2 + dest: /etc/systemd/system/lucke.service + - name: enable service + ansible.builtin.systemd_service: + name: lucke.service + enabled: true + state: restarted + daemon_reload: true + + + - name: daemon reload + become: true + ansible.builtin.systemd_service: + daemon_reload: true diff --git a/ansible_deploy/display/inventory.yml b/ansible_deploy/display/inventory.yml new file mode 100644 index 0000000..d801782 --- /dev/null +++ b/ansible_deploy/display/inventory.yml @@ -0,0 +1,27 @@ +predavalnice_pi: + # Skupne spremenljivke za vse zaslone + vars: + ansible_user: pi + + # Check here: https://github.com/leukipp/touchkio/releases + touchkio_version: "1.3.1" + # Multimedia network + static_ip_cidr: "24" + static_ip_gateway: "192.168.192.1" + static_ip_dns_servers: + - "212.235.188.28" + - "212.235.188.29" + hosts: + p01_touch_display: + predavalnica: p01 + static_ip: "192.168.192.111" + hostname: "p01_touch_display.local" + kiosk_url: "http://192.168.192.42?room=P01" + mqtt_host: "192.168.192.42" + + p22_touch_display: + predavalnica: p22 + static_ip: "192.168.192.112" + hostname: "p22_touch_display.local" + kiosk_url: "http://192.168.192.43?room=P22" + mqtt_host: "192.168.192.43" diff --git a/ansible_deploy/display/pi_stuff.yml b/ansible_deploy/display/pi_stuff.yml new file mode 100644 index 0000000..8130dc2 --- /dev/null +++ b/ansible_deploy/display/pi_stuff.yml @@ -0,0 +1,37 @@ + +# - name: Change splash image +# become: true +# copy: +# src: splash.png +# dest: /usr/share/plymouth/themes/pix/splash.png + +- name: Enable boot splash screen + become: true + shell: "raspi-config nonint get_boot_splash && raspi-config nonint do_boot_splash 0" + register: boot_splash + changed_when: + - boot_splash == "1" + +- name: Disable color splash + become: true + community.general.ini_file: + path: /boot/firmware/config.txt + option: disable_splash + value: 1 + no_extra_spaces: true + +- name: Remove desktop bloat + become: true + apt: + name: + - gvfs + - gnome-keyring + - cups + state: absent + +- name: Switch to wayfire + become: true + shell: "raspi-config nonint is_wayfire && raspi-config nonint do_wayland W2" + register: result + failed_when: ( result.rc not in [ 0, 1 ] ) + changed_when: ( result.rc == 1 ) diff --git a/ansible_deploy/display/playbook.yml b/ansible_deploy/display/playbook.yml new file mode 100644 index 0000000..3f05325 --- /dev/null +++ b/ansible_deploy/display/playbook.yml @@ -0,0 +1,7 @@ +- hosts: predavalnice_pi + # TODO: better include (import playbook) + tasks: + - include_tasks: static_ip.yml +# - include_tasks: wifi_temp.yml # TODO: remove this when we don't need wifi anymore + - include_tasks: pi_stuff.yml + - include_tasks: touch_display.yml diff --git a/ansible_deploy/display/static_ip.yml b/ansible_deploy/display/static_ip.yml new file mode 100644 index 0000000..bea6891 --- /dev/null +++ b/ansible_deploy/display/static_ip.yml @@ -0,0 +1,32 @@ +- name: Configure static IP address (using Network Manager) + become: yes + community.general.nmcli: + conn_name: "Multimedia net" + ifname: eth0 + type: ethernet + ip4: "{{ static_ip }}/{{ static_ip_cidr }}" + # Sorry timi + method6: disabled + # Multimedia net doesn't have Internet access, so this iface shouldn't be used for Internet access + never_default4: true + routes4_extended: + - ip: 192.168.0.0/16 + next_hop: "{{ static_ip_gateway }}" + metric: 9999 + - ip: 10.0.0.0/8 + next_hop: "{{ static_ip_gateway }}" + metric: 9999 + # gw4: "{{ static_ip_gateway }}" + state: present + conn_reload: true + +- name: Wait for network to be available + become: yes + wait_for_connection: + timeout: 60 + +- name: Display new IP configuration + debug: + msg: "Static IP configured: {{ static_ip }}/{{ static_ip_cidr }}" + + diff --git a/ansible_deploy/display/touch_display.yml b/ansible_deploy/display/touch_display.yml new file mode 100644 index 0000000..f95e62f --- /dev/null +++ b/ansible_deploy/display/touch_display.yml @@ -0,0 +1,38 @@ +- name: Fix fonts + become: true + apt: + name: + - fonts-noto-core + state: present + +- name: Download .deb file + get_url: + url: "https://github.com/leukipp/touchkio/releases/download/v{{ touchkio_version }}/touchkio_{{ touchkio_version }}_arm64.deb" + dest: "/home/pi/touchkio_{{ touchkio_version }}_arm64.deb" + register: deb_download + +- name: Install the latest .deb package + become: yes + apt: + deb: "/home/pi/touchkio_{{ touchkio_version }}_arm64.deb" + when: deb_download is succeeded + +- name: Create systemd user service directory + file: + path: "{{ ansible_env.HOME }}/.config/systemd/user" + state: directory + +- name: Create systemd user service + template: + src: touchkio.service.j2 + dest: "{{ ansible_env.HOME }}/.config/systemd/user/touchkio.service" + +- name: Enable systemd service + ansible.builtin.systemd_service: + name: touchkio + enabled: true + state: restarted + scope: user + daemon_reload: true + + when: ansible_check_mode == false diff --git a/ansible_deploy/display/touchkio.service.j2 b/ansible_deploy/display/touchkio.service.j2 new file mode 100644 index 0000000..3cbd11b --- /dev/null +++ b/ansible_deploy/display/touchkio.service.j2 @@ -0,0 +1,12 @@ +[Unit] +Description=Kiosk browser +After=graphical.target + +[Service] +ExecStart=/usr/bin/touchkio --web-url="{{ kiosk_url }}" --web-zoom=1.0 "--mqtt-url=mqtt://{{ mqtt_host }}" +#ExecStart=/usr/bin/chromium-browser --ozone-platform=x11 --noerrdialogs --disable-infobars --kiosk --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --app="{{ kiosk_url }}" +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=default.target diff --git a/ansible_deploy/display/wifi_temp.yml b/ansible_deploy/display/wifi_temp.yml new file mode 100644 index 0000000..4c45096 --- /dev/null +++ b/ansible_deploy/display/wifi_temp.yml @@ -0,0 +1,26 @@ +- name: Set wifi country + become: true + shell: "raspi-config nonint get_wifi_country && raspi-config nonint do_wifi_country SI; true" + register: wifi_country + changed_when: + - wifi_country != "SI" + +- name: Configure P2P wifi + become: yes + community.general.nmcli: + conn_name: "MALINCA" + ifname: wlan0 + type: wifi + ssid: "MALINCA" + wifi_sec: + key-mgmt: wpa-psk + psk: "MALINCA123" + autoconnect: true + method4: auto + state: present + +- name: Restart NetworkManager + become: yes + service: + name: NetworkManager + state: restarted diff --git a/ansible_deploy/inventory.ini b/ansible_deploy/inventory.ini deleted file mode 100644 index da9d595..0000000 --- a/ansible_deploy/inventory.ini +++ /dev/null @@ -1,4 +0,0 @@ -[prHosts] -192.168.122.245 - - diff --git a/ansible_deploy/inventory.yml b/ansible_deploy/inventory.yml deleted file mode 100644 index 121412d..0000000 --- a/ansible_deploy/inventory.yml +++ /dev/null @@ -1,21 +0,0 @@ -P01: - hosts: - 192.168.122.245: - vars: - room: P01 - mqtt_ip: localhost - mqtt_port: 1883 - - barco_G62: - - position: main - model: barco_G62 - port: 3023 - ip: localhost #for testirovanje - # ip: 192.168.192.13 - # - position : side - # model: barco_G62 - # port: 3023 - # ip: 192.168.192.14 - tse_box: - serial_device: /dev/ttyUSB0 - \ No newline at end of file diff --git a/ansible_deploy/malinca.yml b/ansible_deploy/malinca.yml deleted file mode 100644 index edf453a..0000000 --- a/ansible_deploy/malinca.yml +++ /dev/null @@ -1,38 +0,0 @@ -P01: - hosts: - #10.32.50.170: - 192.168.192.42 - vars: - static_ip: 192.168.192.42 - static_mask: 24 - static_routers: 192.168.192.1 - static_nameservers: 192.168.192.1 - room: P01 - mqtt_ip: localhost - mqtt_port: 1883 - - barco_G62: - - position: main - model: barco_G62 - port: 3023 - # ip: localhost #for testirovanje - ip: 192.168.192.12 - - position : side - model: barco_G62 - port: 3023 - ip: 192.168.192.13 - tse_box: - serial_device: /dev/ttyUSB0 - - projector_motors: - - position: main - i2c_address: 0x42 - offset: 0 #for when using single 8 channel relay board - - position: side - i2c_address: 0x43 - offset: 4 - - lucke: - url: http://192.168.190.90:8091/rest/fri-fkkt/lecture-halls/{roomId}/scenes/{sceneId}/activate - roomId: 0 - bearer_token: 0954afe1-4111-4f89-a123-fea08a55dc46 diff --git a/ansible_deploy/playbook.yaml b/ansible_deploy/playbook.yaml deleted file mode 100644 index cb79108..0000000 --- a/ansible_deploy/playbook.yaml +++ /dev/null @@ -1,211 +0,0 @@ -- name: Test playbook - vars: - # TODO: maybe don't hardcode this? - PROJECT_BASE: "/home/kat/Documents/polzp/fri_multimedia_rework" - hosts: P01 - - # vars_files: - # - secret - - # addr: "192.168.122.245" - - handlers: - - name: restart NM - ansible.builtin.service: - name: NetworkManager - state: restarted - - tasks: - - name: Ping hosts - ansible.builtin.ping: - - - - name: set eth0 static IP - become: true - community.general.nmcli: - conn_name: "Multimedia network" - ifname: eth0 - type: ethernet - ip4: "{{ static_ip }}/{{ static_mask }}" - gw4: "{{ static_routers }}" - state: present - #notify: restart NM - - - - name: Install pkgs - become: true - apt: - name: - - python3-pip - - mosquitto - - nginx - state: latest - - - name: pip install - pip: - break_system_packages: true - name: - - poetry - - - - name: check for script directory - file: - path: "/home/pi/pyServices" - state: directory - - - - name: Copy poetry conf - #become: true - ansible.builtin.copy: - #seuser: root - src: "../{{ item }}" - dest: "/home/pi/pyServices/{{ item }}" - owner: pi - group: pi - mode: '0644' - backup: yes - loop: - - poetry.lock - - pyproject.toml - - README.md - - - - name: template config.toml - ansible.builtin.template: - src: ./conf.j2 - dest: /home/pi/pyServices/malinaConfig.toml - - - name: copy python scripts - #become: true - ansible.builtin.copy: - src: "../{{ item }}" - dest: "/home/pi/pyServices" - owner: pi - group: pi - mode: '0644' - backup: yes - loop: - - barco_telnet/barco_G62_control.py - - extron_audio_matrix/extron_audio_matrix_telnet_control.py - - extron_audio_matrix/extron_audio_matrix_telnet_interpreter.py - - projector_motors/projector_motors.py - - tse_serial/tse_serial_controler.py - - tse_serial/tse_serial_interpreter.py - - lucke/luckeControl.py - # - config.toml # bruh - - - - name: poetry installation - ansible.builtin.shell: - cmd: "poetry install" - chdir: "/home/pi/pyServices" - - - name: mosquitto service and reload - become: true - ansible.builtin.systemd_service: - name: mosquitto.service - state: restarted - daemon_reload: true - - - - name: generate systemd services - become: true - block: - - name: barco services - when: barco_G62 is defined - block: - - name: template service - vars: - script_file: "/home/pi/pyServices/barco_G62_control.py %i" - ansible.builtin.template: - src: ./service.j2 - dest: /lib/systemd/system/barco@.service - - name: enable service - ansible.builtin.systemd_service: - name: "{{item}}" - enabled: true - state: restarted - loop: - - barco@main.service - - barco@side.service - - - name: template projector motors service - when: projector_motors is defined - block: - - name: template service - vars: - script_file: "/home/pi/pyServices/projector_motors.py" - ansible.builtin.template: - src: ./service.j2 - dest: /lib/systemd/system/projector_motors.service - - name: enable service - ansible.builtin.systemd_service: - name: projector_motors.service - enabled: true - state: restarted - - - name: template tse serial box service - when: tse_box is defined - block: - - name: template service - vars: - script_file: "/home/pi/pyServices/tse_serial_controler.py" - ansible.builtin.template: - src: ./service.j2 - dest: /lib/systemd/system/tse_box.service - - name: enable service - ansible.builtin.systemd_service: - name: tse_box.service - enabled: true - state: restarted - - - name: template lucke service - when: lucke is defined - block: - - name: tmeplate service - vars: - script_file: "/home/pi/pyServices/luckeControl.py" - ansible.builtin.template: - src: ./service.j2 - dest: /lib/systemd/system/lucke.service - - name: enable service - ansible.builtin.systemd_service: - name: lucke.service - enabled: true - state: restarted - - - - - name: Copy mosqitconfig - become: true - ansible.builtin.copy: - #seuser: root - src: ../mosquitto.conf - dest: /etc/mosquitto/conf.d - owner: root - group: root - mode: '0644' - backup: yes - - - - name: Build frontend - delegate_to: localhost - command: - chdir: "{{PROJECT_BASE}}/frontend/vju_display/" - cmd: "npm run build" - - - name: copy frontend to webroot - become: true - - ansible.builtin.copy: - src: "{{PROJECT_BASE}}/frontend/vju_display/dist/" - dest: /var/www/html/ - owner: root - group: www-data - mode: '0755' - backup: yes - - - name: daemon reload - become: true - ansible.builtin.systemd_service: - daemon_reload: true diff --git a/ansible_deploy/testbook.yaml b/ansible_deploy/testbook.yaml deleted file mode 100644 index 1082599..0000000 --- a/ansible_deploy/testbook.yaml +++ /dev/null @@ -1,38 +0,0 @@ -- name: test playbook - hosts: P01 - - #vars_files: - # - secret - vars: - addr: "192.168.122.245" - os_environment: - - key: VITE_MQTT_HOST - value: polztest.local - - tasks: - - name: test things - when: barco_G62 is defined - block: - #- name: ping hosts - # ansible.builtin.ping: - - - name: template config.toml - ansible.builtin.template: - src: ./conf.j2 - dest: /home/pi/conf.toml - - name: template barco systemd service - vars: - script_file: "/home/kat/pyServices/fri-mm-maline/barco_G62_control.py %i" - ansible.builtin.template: - src: ./service.j2 - dest: /home/kat/testo/barco.service - - name: enable barcos - ansible.builtin.ping: - #itd itd itd - - name: test second - when: barco_old is defined - block: - - name: pingerino - ansible.builtin.ping: - - diff --git a/barco.service b/barco.service deleted file mode 100644 index 653b8b8..0000000 --- a/barco.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Barco projector control -After=mqtt_init.service - - -[Service] -ExecStart=/usr/bin/python3 /home/rpi/barco_telnet_control.py -User=rpi -Group=rpi -Type=simple -Restart=always - - -[Install] -WantedBy=mqtt_init.service - diff --git a/barco@.service b/barco@.service deleted file mode 100644 index 99f9c39..0000000 --- a/barco@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Barco projector control -After=multi-user.target - -[Service] -ExecStart=/usr/bin/poetry run python3 /home/kat/pyServices/fri-mm-maline/barco_G62_control.py %i -Type=simple -Restart=always -WorkingDirectory=/home/kat/pyServices/fri-mm-maline -User=kat -Group=kat -RestartSec=10 - -[Install] -WantedBy=multi-user.target -DefaultInstance=main \ No newline at end of file diff --git a/barco_telnet/barco_G62_control.py b/barco_telnet/barco_G62_control.py deleted file mode 100644 index 14031c1..0000000 --- a/barco_telnet/barco_G62_control.py +++ /dev/null @@ -1,142 +0,0 @@ -import asyncio -import socket -import aiomqtt -import telnetlib3 -import toml -import sys - -#GLOBALS - -room: str -barcoPosition: str -barcoIP: str -telnetPort: int -mqttPort: int -mqttIP: str -barcoReached: bool - -cmdMap = { - 'power': 'POWR', - 'shutter': 'PMUT', - 'freeze': 'FRZE' -} -reverseCmdMap = {v: k for k, v in cmdMap.items()} - - -def parse_barco_response(raw: str): - global room, barcoPosition, barcoReached - raw = raw[1:-1] # strip square brackets - #print(raw) - if raw.startswith("ERR"): - print("Projector" + room + " " + barcoPosition + " returned" + raw) - return None # TODO parse type error - "disabled control" is special case which shouldnt normally happen - cmd, status = raw.split("!", maxsplit=2) - #print(cmd + " " + status) - cmd = reverseCmdMap[cmd] - status = '1' if status == '01' else '0' - barcoReached = True - return cmd, status - - -async def barco_telnet_command(client, writer, select: str): - global room - onSub = f"{room}/projectors/{select}/#" - #print('TEST', onSub) - onMatch = f"{room}/projectors/{select}/command/+" #TODO should be set? - await client.subscribe(onSub) - #async with client.messages as msgs: - async for mesg in client.messages: - # print(mesg.topic.value) - # print(mesg.payload.decode()) - # print('on', select) - - if mesg.topic.matches(onMatch): - # print("test") - cmd = mesg.topic.value.split("/")[-1] - #val = '1' if mesg.payload.decode() == 'ON' else '0' - val = '1' if mesg.payload.decode() == '1' else '0' # refactor to direct 0 and 1 - barcoCmd = f"[{cmdMap[cmd]}{val}]" - print("Received: [" + mesg.topic.value + "] payload: [" + mesg.payload.decode() + "]") - print("Sending command to Barco: " + barcoCmd) - writer.write(barcoCmd) - - -async def barco_telnet_read_status(client, reader, select: str): - global room - while True: - output = await reader.readuntil(b']') - raw_response: str = output.decode().strip() # strip not necessary? needed for local netcat testing though - print("Received: " + raw_response + " from Barco (" + select + ')') - parsed = parse_barco_response(raw_response) - if parsed is None: - continue #TODO alert for errors - print(f"Updating topic [{parsed[0]}] with value [{parsed[1]}]") - await client.publish(f"{room}/projectors/{select}/status/{parsed[0]}", payload=parsed[1]) - - -async def barco_telnet_query_status(writer, select: str): - while True: - for val in cmdMap.values(): - print(f"Querying Barco {select} with: [{val}?]") - writer.write(f"[{val}?]" + '\r\n') # TODO test if funny CRLF necessary (it probably gets ignored) - await asyncio.sleep(0.2) # sleep between writes necessary, otherwise it gets confused. - # simultaneous commands from control could break this? TODO fix later - await asyncio.sleep(10) # TODO find appropriate period - - -# async def shell(reader, writer): -# async with aiomqtt.Client('localhost', 1883) as client: -# task_status_query = asyncio.create_task(barco_telnet_query_status(writer)) -# task_status_reader = asyncio.create_task(barco_telnet_read_status(client, reader)) -# task_control = asyncio.create_task(barco_telnet_command(client, writer)) -# await asyncio.gather(task_status_query, task_status_reader, task_control) - - -async def main(): - global room, barcoReached - global barcoPosition, barcoIP, telnetPort, mqttIP, mqttPort - if len(sys.argv) < 2: - sys.exit("No position provided") - else: - barcoPosition = sys.argv[1] - - conf = toml.load('./malinaConfig.toml') - g62Barcos = {k: v for k,v in conf["barco_G62"].items()} - currentBarco = g62Barcos[barcoPosition] - barcoIP = currentBarco['ip'] - telnetPort = int(currentBarco["port"]) - - room = conf["global"]["room"] - mqttPort = int(conf["global"]["mqttPort"]) - mqttIP = conf["global"]["mqttIp"] - barcoReached = False - try: - barcoReader, barcoWriter = await telnetlib3.open_connection(barcoIP, telnetPort) - barcoReached = True - except Exception as e: - print("Connection failed: " + barcoIP + ": " + str(e)) - barcoReached = False - finally: - async with aiomqtt.Client(mqttIP, mqttPort) as client: - task_status_query_barco = asyncio.create_task(barco_telnet_query_status(barcoWriter, barcoPosition)) - task_status_reader_barco = asyncio.create_task(barco_telnet_read_status(client, barcoReader, barcoPosition)) - task_control_barco = asyncio.create_task(barco_telnet_command(client, barcoWriter, barcoPosition)) - - await asyncio.gather(task_status_query_barco, task_status_reader_barco, task_control_barco) - await client.publish(f"{room}/projectors/{barcoPosition}/error", payload=("UNREACHABLE" if not barcoReached else "OK")) - -### fuj to, ne tk delat - -# if __name__ == '__main__': - -# loop = asyncio.get_event_loop() -# coro = telnetlib3.open_connection(mainBarcoIP, 3023, shell=shell) -# coro = telnetlib3.open_connection(mainBarcoIP, 3023, shell=shell) -# # coro = telnetlib3.open_connection('localhost', 1234, shell=shell) -# reader, writer = loop.run_until_complete(coro) -# loop.run_until_complete(writer.protocol.waiter_closed) - -#mqttIP = sys.argv[1] -#barcoIP = sys.argv[2] - -asyncio.run(main()) \ No newline at end of file diff --git a/config.toml b/config.toml deleted file mode 100644 index e08fbdf..0000000 --- a/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[globals] -room = 'P01' -mqttIp = 'localhost' -mqttPort = '1883' - - -[barco.novi.glavni] -ip = '192.168.192.12' -port = '3023' - -[barco.novi.stranski] -ip = '192.168.192.16' # or smth -port = '3023' - diff --git a/barco_rlmw_http/barco_rlwm_http.py b/controller/barco_rlmw_http/barco_rlwm_http.py similarity index 100% rename from barco_rlmw_http/barco_rlwm_http.py rename to controller/barco_rlmw_http/barco_rlwm_http.py diff --git a/barco_rlmw_http/main.py b/controller/barco_rlmw_http/main.py similarity index 100% rename from barco_rlmw_http/main.py rename to controller/barco_rlmw_http/main.py diff --git a/barco_rlmw_tcp/barco_rlmw_tcp.py b/controller/barco_rlmw_tcp/barco_rlmw_tcp.py similarity index 100% rename from barco_rlmw_tcp/barco_rlmw_tcp.py rename to controller/barco_rlmw_tcp/barco_rlmw_tcp.py diff --git a/barco_rlmw_tcp/main.py b/controller/barco_rlmw_tcp/main.py similarity index 100% rename from barco_rlmw_tcp/main.py rename to controller/barco_rlmw_tcp/main.py diff --git a/controller/barco_telnet/barco_G62_control.py b/controller/barco_telnet/barco_G62_control.py new file mode 100644 index 0000000..b2d6976 --- /dev/null +++ b/controller/barco_telnet/barco_G62_control.py @@ -0,0 +1,138 @@ +import asyncio +from collections import defaultdict +import aiomqtt +import telnetlib3 +import toml +import sys +import os + +#GLOBALS + +room: str +barcoPosition: str +barcoReached: bool + +lastState = defaultdict(lambda: None) + +cmdMap = { + 'power': 'POWR', + 'shutter': 'PMUT', + 'freeze': 'FRZE', + #'test_pattern': 'TPRN', +} +reverseCmdMap = {v: k for k, v in cmdMap.items()} + +# There needs to be a minimum time between writes. Since we have two "threads", we use a lock and a sleep in barco_send() to enforce it +lock = asyncio.Lock() + +def parse_barco_response(raw: str): + raw = raw[1:-1] # strip square brackets + + if raw.startswith("ERR"): + print("ERROR:", raw) + return None # TODO parse type error - "disabled control" is special case which shouldnt normally happen + + cmd, status = raw.split("!", maxsplit=2) + + cmd = reverseCmdMap[cmd] + status = int(status) + + barcoReached = True + return cmd, status + + +async def barco_send(writer, value): + async with lock: + writer.write(value + '\r\n') + print("Writing", value) + await asyncio.sleep(0.2) # sleep between writes necessary, otherwise it gets confused. + +async def barco_telnet_command(client, writer, select: str): + """Receive commands from MQTT and send them to the projector""" + await client.subscribe(f"{room}/projectors/{select}/#") + + async for mesg in client.messages: + + if mesg.topic.matches(f"{room}/projectors/{select}/set/+"): + cmd = mesg.topic.value.rsplit("/", maxsplit=1)[-1] + val = mesg.payload.decode() + + if val not in ("0", "1") or cmd not in cmdMap: + print("INVALID COMMAND OR VALUE:", cmd, val) + continue + + barcoCmd = cmdMap[cmd] + # Send command to projector + await barco_send(writer, f"[{barcoCmd}{val}]") + # Immediately ask for a status + await barco_send(writer, f"[{barcoCmd}?]") + + +async def barco_telnet_read_status(client, reader, select: str): + """Read status reports (we trigger them in the polling task as well as whenver sending a command)""" + while True: + output = await reader.readuntil(b']') + raw_response: str = output.decode() + print("Received: " + raw_response + " from Barco (" + select + ')') + + try: + key, val = parse_barco_response(raw_response) + except: + print("NOT PARSED:", raw_response) + continue + + await client.publish(f"{room}/projectors/{select}/status/{key}", payload=val) + + +async def barco_telnet_query_status(writer, select: str): + """Periodically ask the projector for its status""" + while True: + # Most queries only work when turned on, so if we're not sure, only query power + if lastState[cmdMap["power"]] == "01": + queries = cmdMap.values() + else: + queries = [cmdMap["power"]] + + for val in queries: + await barco_send(writer, f"[{val}?]") + + await asyncio.sleep(2) # TODO find appropriate period + + +async def main(): + global room, barcoReached, barcoPosition + if len(sys.argv) < 2: + sys.exit("No position provided") + else: + barcoPosition = sys.argv[1] + + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + g62Barcos = {k: v for k,v in conf["barco_G62"].items()} + currentBarco = g62Barcos[barcoPosition] + barcoIP = currentBarco['ip'] + telnetPort = int(currentBarco["port"]) + + barcoReached = False + try: + barcoReader, barcoWriter = await telnetlib3.open_connection(barcoIP, telnetPort) + barcoReached = True + except Exception as e: + print("Connection failed: " + barcoIP + ": " + str(e)) + barcoReached = False + else: + async with aiomqtt.Client(mqttHost, mqttPort) as client: + task_status_query_barco = asyncio.create_task(barco_telnet_query_status(barcoWriter, barcoPosition)) + task_status_reader_barco = asyncio.create_task(barco_telnet_read_status(client, barcoReader, barcoPosition)) + task_control_barco = asyncio.create_task(barco_telnet_command(client, barcoWriter, barcoPosition)) + + await asyncio.gather(task_status_query_barco, task_status_reader_barco, task_control_barco) + await client.publish(f"{room}/projectors/{barcoPosition}/error", payload=("UNREACHABLE" if not barcoReached else "OK")) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/extron_audio_matrix/extron_audio_matrix_telnet_control.py b/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py similarity index 89% rename from extron_audio_matrix/extron_audio_matrix_telnet_control.py rename to controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py index 52a8180..ae71d7e 100644 --- a/extron_audio_matrix/extron_audio_matrix_telnet_control.py +++ b/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py @@ -47,12 +47,17 @@ async def shell(reader, writer): async with aiomqtt.Client('localhost', 1883) as client: task_status_query = asyncio.create_task(extron_audio_telnet_status(client, writer, reader)) task_control = asyncio.create_task(extron_audio_telnet_control(client, writer)) - await asyncio.gather(task_control) + await asyncio.gather(task_status_query, task_control) if __name__ == '__main__': + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + loop = asyncio.get_event_loop() - #coro = telnetlib3.open_connection('localhost', 1234, shell=shell) coro = telnetlib3.open_connection('192.168.192.14', 23, shell=shell) reader, writer = loop.run_until_complete(coro) loop.run_until_complete(writer.protocol.waiter_closed) diff --git a/extron_audio_matrix/extron_audio_matrix_telnet_interpreter.py b/controller/extron_audio_matrix/extron_audio_matrix_telnet_interpreter.py similarity index 100% rename from extron_audio_matrix/extron_audio_matrix_telnet_interpreter.py rename to controller/extron_audio_matrix/extron_audio_matrix_telnet_interpreter.py diff --git a/controller/lucke/luckeControl.py b/controller/lucke/luckeControl.py new file mode 100644 index 0000000..60fece0 --- /dev/null +++ b/controller/lucke/luckeControl.py @@ -0,0 +1,110 @@ +from http import HTTPStatus + +import aiomqtt +import asyncio +import toml +import aiohttp +import os + +lucke_bearer_token: str +room: str +url: str +roomId: int|str + +async def sendSceneRequest(client, scene): + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/scenes/{scene}/activate" + async with aiohttp.request("GET", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}) as resp: + if resp.status == 204: + await client.publish(f'{room}/lucke/preset/current', payload=scene, qos=1, retain=True) + else: + print(resp.status, await resp.text()) + + +async def setLight(client, lightNum, intensity: int): + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/lights/{lightNum}" + async with aiohttp.request("PUT", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}, json={ + "stateOn": intensity != 0, + "dimmValue": intensity + }) as resp: + if resp.status == 204: + await client.publish(f'{room}/lucke/brightness/{lightNum}', payload=intensity, qos=1, retain=True) + else: + print("setLight error:", resp.status, await resp.text()) + +async def saveScene(client, sceneNum): + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/scenes/{sceneNum}/save" + async with aiohttp.request("GET", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}) as resp: + if resp.status == 204: + pass + else: + print("saveScene error:", resp.status, await resp.text()) + +async def task_luckeCommand(controlClient): + await controlClient.subscribe(f"{room}/lucke/#") + + async for mesg in controlClient.messages: + mesg: aiomqtt.Message + + if mesg.topic.matches(f"{room}/lucke/preset/recall"): + sceneNum = mesg.payload.decode() + print("Received: " + str(mesg.topic) + " payload: [" + sceneNum + "]") + await sendSceneRequest(controlClient, sceneNum) + + elif mesg.topic.matches(f"{room}/lucke/preset/save"): + sceneNum = mesg.payload.decode() + print("Received: " + str(mesg.topic) + " payload: [" + sceneNum + "]") + await saveScene(controlClient, sceneNum) + + elif mesg.topic.matches(f"{room}/lucke/set/+"): + lightNum = mesg.topic.value.rsplit("/", maxsplit=1)[-1] + try: + intensity = int(mesg.payload.decode()) + assert 0 <= intensity <= 100 + except: + print("Invalid message:", mesg) + else: + await setLight(controlClient, lightNum, intensity) + + await asyncio.sleep(0.01) + +async def task_luckePoll(client): + """Polls the API and sends light brightness to the API""" + while True: + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/lights/" + async with aiohttp.ClientSession() as session: + async with session.get(endpoint, headers={"Authorization": f"Bearer {lucke_bearer_token}"}) as response: + if response.status == 200: + lights = await response.json() + for light in lights: + await client.publish(f'{room}/lucke/is_dimmable/{light["id"]}', payload=light["dimmable"], qos=1, retain=True) + # TODO: find a better way to handle non-dimmable lights + if light["stateOn"]: + dimValue = light.get("dimmValue", 100) + else: + dimValue = 0 + await client.publish(f'{room}/lucke/brightness/{light["id"]}', payload=dimValue, qos=1, retain=True) + + else: + print(f"Failed to fetch lights: {response.status}", await response.text()) + await asyncio.sleep(.5) + +async def main(): + global room, lucke_bearer_token, url, roomId + + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + url = conf["lucke"]['url'] + roomId = conf["lucke"]['roomId'] + lucke_bearer_token = conf["lucke"]['bearer_token'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: + task_control = asyncio.create_task(task_luckeCommand(client)) + task_control = asyncio.create_task(task_luckePoll(client)) + await asyncio.gather(task_control) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/controller/malinaConfig.toml b/controller/malinaConfig.toml new file mode 100644 index 0000000..014f231 --- /dev/null +++ b/controller/malinaConfig.toml @@ -0,0 +1,17 @@ +[global] +mqttHost = '192.168.192.42' +mqttPort = 1883 +room = 'P01' + +[lucke] +url = 'http://192.168.190.90' +roomId = 1 +bearer_token = '0954afe1-4111-4f89-a123-fea08a55dc46' + +[barco_G62.main] +ip = '192.168.192.12' +port = 3023 + +[barco_G62.side] +ip = '192.168.192.13' +port = 3023 diff --git a/poetry.lock b/controller/poetry.lock similarity index 77% rename from poetry.lock rename to controller/poetry.lock index 86d6cc1..6214d70 100644 --- a/poetry.lock +++ b/controller/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -17,6 +18,7 @@ version = "3.12.13" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29"}, {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0"}, @@ -117,7 +119,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiomqtt" @@ -125,6 +127,7 @@ version = "2.3.0" description = "The idiomatic asyncio MQTT client, wrapped around paho-mqtt" optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "aiomqtt-2.3.0-py3-none-any.whl", hash = "sha256:127926717bd6b012d1630f9087f24552eb9c4af58205bc2964f09d6e304f7e63"}, {file = "aiomqtt-2.3.0.tar.gz", hash = "sha256:312feebe20bc76dc7c20916663011f3bd37aa6f42f9f687a19a1c58308d80d47"}, @@ -139,6 +142,7 @@ version = "1.3.1" description = "An asynchronous serial port library of Python" optional = false python-versions = ">=3.6,<4.0" +groups = ["main"] files = [ {file = "aioserial-1.3.1.tar.gz", hash = "sha256:702bf03b0eb84b8ef2d8dac5cb925e1e685dce98f77b125569bc6fd2b3b58228"}, ] @@ -152,6 +156,7 @@ version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, @@ -166,6 +171,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -177,18 +184,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "colorzero" @@ -196,6 +204,7 @@ version = "2.0" description = "Yet another Python color library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "colorzero-2.0-py2.py3-none-any.whl", hash = "sha256:0e60d743a6b8071498a56465f7719c96a5e92928f858bab1be2a0d606c9aa0f8"}, {file = "colorzero-2.0.tar.gz", hash = "sha256:e7d5a5c26cd0dc37b164ebefc609f388de24f8593b659191e12d85f8f9d5eb58"}, @@ -214,6 +223,7 @@ version = "1.7.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, @@ -327,6 +337,7 @@ version = "2.0.1" description = "A simple interface to GPIO devices with Raspberry Pi" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "gpiozero-2.0.1-py3-none-any.whl", hash = "sha256:8f621de357171d574c0b7ea0e358cb66e560818a47b0eeedf41ce1cdbd20c70b"}, {file = "gpiozero-2.0.1.tar.gz", hash = "sha256:d4ea1952689ec7e331f9d4ebc9adb15f1d01c2c9dcfabb72e752c9869ab7e97e"}, @@ -345,6 +356,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -355,121 +367,122 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "multidict" -version = "6.5.0" +version = "6.6.4" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "multidict-6.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e118a202904623b1d2606d1c8614e14c9444b59d64454b0c355044058066469"}, - {file = "multidict-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a42995bdcaff4e22cb1280ae7752c3ed3fbb398090c6991a2797a4a0e5ed16a9"}, - {file = "multidict-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2261b538145723ca776e55208640fffd7ee78184d223f37c2b40b9edfe0e818a"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e5b19f8cd67235fab3e195ca389490415d9fef5a315b1fa6f332925dc924262"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:177b081e4dec67c3320b16b3aa0babc178bbf758553085669382c7ec711e1ec8"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d30a2cc106a7d116b52ee046207614db42380b62e6b1dd2a50eba47c5ca5eb1"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a72933bc308d7a64de37f0d51795dbeaceebdfb75454f89035cdfc6a74cfd129"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d109e663d032280ef8ef62b50924b2e887d5ddf19e301844a6cb7e91a172a6"}, - {file = "multidict-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b555329c9894332401f03b9a87016f0b707b6fccd4706793ec43b4a639e75869"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6994bad9d471ef2156f2b6850b51e20ee409c6b9deebc0e57be096be9faffdce"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b15f817276c96cde9060569023808eec966bd8da56a97e6aa8116f34ddab6534"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b4bf507c991db535a935b2127cf057a58dbc688c9f309c72080795c63e796f58"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:60c3f8f13d443426c55f88cf3172547bbc600a86d57fd565458b9259239a6737"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a10227168a24420c158747fc201d4279aa9af1671f287371597e2b4f2ff21879"}, - {file = "multidict-6.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e3b1425fe54ccfde66b8cfb25d02be34d5dfd2261a71561ffd887ef4088b4b69"}, - {file = "multidict-6.5.0-cp310-cp310-win32.whl", hash = "sha256:b4e47ef51237841d1087e1e1548071a6ef22e27ed0400c272174fa585277c4b4"}, - {file = "multidict-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:63b3b24fadc7067282c88fae5b2f366d5b3a7c15c021c2838de8c65a50eeefb4"}, - {file = "multidict-6.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:8b2d61afbafc679b7eaf08e9de4fa5d38bd5dc7a9c0a577c9f9588fb49f02dbb"}, - {file = "multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95"}, - {file = "multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea"}, - {file = "multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee"}, - {file = "multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572"}, - {file = "multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877"}, - {file = "multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138"}, - {file = "multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0"}, - {file = "multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb"}, - {file = "multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74"}, - {file = "multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653"}, - {file = "multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955"}, - {file = "multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462"}, - {file = "multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf"}, - {file = "multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851"}, - {file = "multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743"}, - {file = "multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35"}, - {file = "multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456"}, - {file = "multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99"}, - {file = "multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75"}, - {file = "multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a"}, - {file = "multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b"}, - {file = "multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af"}, - {file = "multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06"}, - {file = "multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2"}, - {file = "multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a"}, - {file = "multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676"}, - {file = "multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887"}, - {file = "multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068"}, - {file = "multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461"}, - {file = "multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1"}, - {file = "multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1"}, - {file = "multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4"}, - {file = "multidict-6.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0078358470da8dc90c37456f4a9cde9f86200949a048d53682b9cd21e5bbf2b"}, - {file = "multidict-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cc7968b7d1bf8b973c307d38aa3a2f2c783f149bcac855944804252f1df5105"}, - {file = "multidict-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad73a60e11aa92f1f2c9330efdeaac4531b719fc568eb8d312fd4112f34cc18"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3233f21abdcd180b2624eb6988a1e1287210e99bca986d8320afca5005d85844"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bee5c0b79fca78fd2ab644ca4dc831ecf793eb6830b9f542ee5ed2c91bc35a0e"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e053a4d690f4352ce46583080fefade9a903ce0fa9d820db1be80bdb9304fa2f"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42bdee30424c1f4dcda96e07ac60e2a4ede8a89f8ae2f48b5e4ccc060f294c52"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58b2ded1a7982cf7b8322b0645713a0086b2b3cf5bb9f7c01edfc1a9f98d20dc"}, - {file = "multidict-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f805b8b951d1fadc5bc18c3c93e509608ac5a883045ee33bc22e28806847c20"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2540395b63723da748f850568357a39cd8d8d4403ca9439f9fcdad6dd423c780"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c96aedff25f4e47b6697ba048b2c278f7caa6df82c7c3f02e077bcc8d47b4b76"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e80de5ad995de210fd02a65c2350649b8321d09bd2e44717eaefb0f5814503e8"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6cb9bcedd9391b313e5ec2fb3aa07c03e050550e7b9e4646c076d5c24ba01532"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a7d130ed7a112e25ab47309962ecafae07d073316f9d158bc7b3936b52b80121"}, - {file = "multidict-6.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:95750a9a9741cd1855d1b6cb4c6031ae01c01ad38d280217b64bfae986d39d56"}, - {file = "multidict-6.5.0-cp39-cp39-win32.whl", hash = "sha256:7f78caf409914f108f4212b53a9033abfdc2cbab0647e9ac3a25bb0f21ab43d2"}, - {file = "multidict-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:220c74009507e847a3a6fc5375875f2a2e05bd9ce28cf607be0e8c94600f4472"}, - {file = "multidict-6.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:d98f4ac9c1ede7e9d04076e2e6d967e15df0079a6381b297270f6bcab661195e"}, - {file = "multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc"}, - {file = "multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, + {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, + {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, + {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, + {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, + {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, + {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, + {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, + {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, + {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, + {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, + {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, + {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, + {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, + {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, + {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, + {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, + {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, + {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, + {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, + {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, ] [package.dependencies] @@ -481,6 +494,7 @@ version = "2.1.0" description = "MQTT version 5.0/3.1.1 client class" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee"}, {file = "paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834"}, @@ -495,6 +509,7 @@ version = "0.3.2" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, @@ -602,6 +617,7 @@ version = "3.5" description = "Python Serial Port Extension" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, @@ -616,19 +632,20 @@ version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.7.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "smbus2" @@ -636,6 +653,7 @@ version = "0.5.0" description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "smbus2-0.5.0-py2.py3-none-any.whl", hash = "sha256:1a15c3b9fa69357beb038cc0b5d37939702f8bfde1ddc89ca9f17d8461dbe949"}, {file = "smbus2-0.5.0.tar.gz", hash = "sha256:4a5946fd82277870c2878befdb1a29bb28d15cda14ea4d8d2d54cf3d4bdcb035"}, @@ -651,6 +669,7 @@ version = "2.0.4" description = "Python 3 asyncio Telnet server and client Protocol library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "telnetlib3-2.0.4-py2.py3-none-any.whl", hash = "sha256:b3c0f984a7fb1b6ee16e6fdaa410c56389b0dc492174a99c6661b1ba4c9d457d"}, {file = "telnetlib3-2.0.4.tar.gz", hash = "sha256:dbcbc16456a0e03a62431be7cfefff00515ab2f4ce2afbaf0d3a0e51a98c948d"}, @@ -662,6 +681,7 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -673,6 +693,8 @@ version = "4.14.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, @@ -684,6 +706,7 @@ version = "1.20.1" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, @@ -797,6 +820,6 @@ multidict = ">=4.0" propcache = ">=0.2.1" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "a7ae9d972673e97295d579159de1fb320896ed53913cf68f5b270c50bd271e03" +content-hash = "b0cd468209f60a9f821063be3900cc54677b0e93b7959a05971c1594d9d8c06d" diff --git a/controller/projector_motors/projector_motors.py b/controller/projector_motors/projector_motors.py new file mode 100644 index 0000000..4a4a48b --- /dev/null +++ b/controller/projector_motors/projector_motors.py @@ -0,0 +1,147 @@ +MOCK = False +import aiomqtt +import asyncio +from smbus2 import SMBus +import toml +import os + +room: str +use_offset = False + +""" 0 1 2 3 """ +relayMasks = [0b0001, 0b0010, 0b0100, 0b1000] #probably ne rabim + +bus = SMBus(1) + +i2c_map = { + 'main': -1, + 'side': -1, +} + +relMapping = { + 'service_down': 0, + 'service_up': 1, + 'down': 2, + 'up': 3 +} + +currentState = { + 'main': 0b0000, + 'side': 0b0000 +} + +async def msgRelayBoard(projSelect, command, state: bool): + # Select the correct relay board + i2c_addr = i2c_map[projSelect] + + # Get the relay position for the given command + maskShift = relMapping[command] + # Set the corresponding bit + mask = (1 << maskShift) + if state: + currentState[projSelect] = currentState[projSelect] | mask + else: + currentState[projSelect] = currentState[projSelect] & ~mask + + # Write it to the I2C bus (0x10 is the register for relay states) + bus.write_byte_data(i2c_addr, 0x10, currentState[projSelect]) + print(projSelect, "{:04b}".format(currentState[projSelect])) + + +""" + SrvDwn SrvUp OpDwn OpUp +MAIN: 0x42 0001 0010 0100 1000 +SIDE: 0x43 0001 0010 0100 1000 +""" + + +#old board +""" +MAIN: SrvDwn SrvUp OpDwn OpUp + 0 1 2 3 +SIDE: 4 5 6 7 +""" +#dej like bolš to podukumentiraj or smth + +async def task_command2relays(controlClient: aiomqtt.Client): + """Read commands from MQTT and send them to the relays""" + + await controlClient.subscribe(f"{room}/projectors/+/lift/#") + + async for mesg in controlClient.messages: + msgTopicArr = mesg.topic.value.split('/') + value = mesg.payload.decode() + + if mesg.topic.matches(f'{room}/projectors/+/lift/manual/+'): + command = msgTopicArr[-1] + projSel = msgTopicArr[-4] + + if projSel not in ("main", "side") or command not in relMapping.keys() or value not in ("0", "1"): + print("Invalid manual command:", projSel, command, value) + continue + + await msgRelayBoard(projSel, command, value == '1') + + # Service move + if "service" in command: + # Manual control makes the position unknown, so we clear it + if value == "1": + status = "MOVING" + else: + status = "STOPPED" + await controlClient.publish(f'{room}/projectors/{projSel}/lift/status', payload=status, qos=1, retain=True) + # Normal move + else: + # HACK: if the press is too short it doesn't register, so we sleep for a bit + if value == "1": + await asyncio.sleep(.2) + + #print("Pushing \'off\' to other relays to prevent conflicts") + + elif mesg.topic.matches(f'{room}/projectors/+/lift/goto'): + projSel = msgTopicArr[-3] + + if projSel not in ("main", "side") or value not in ("UP", "DOWN"): + print("Invalid goto command:", projSel, value) + continue + + # Clear manual control + await msgRelayBoard(projSel, "service_down", False) + await msgRelayBoard(projSel, "service_up", False) + + if value == "UP": + other = "down" + elif value == "DOWN": + other = "up" + await msgRelayBoard(projSel, other, False) + + # Click the buttom for a bit and release it, then publish that the lift has moved + await msgRelayBoard(projSel, value.lower(), True) + await asyncio.sleep(1) + await msgRelayBoard(projSel, value.lower(), False) + + await controlClient.publish(f'{room}/projectors/{projSel}/lift/status', payload=value, qos=1, retain=True) + + await asyncio.sleep(0.01) + + + +async def main(): + global i2c_map, room + + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + projMotors = conf["projector_motors"] + i2c_map['main'] = projMotors["main"]['i2c_address'] + i2c_map['side'] = projMotors["side"]['i2c_address'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: + task_control = asyncio.create_task(task_command2relays(client)) + await asyncio.gather(task_control) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/pyproject.toml b/controller/pyproject.toml similarity index 95% rename from pyproject.toml rename to controller/pyproject.toml index 89ce56d..fe61ac2 100644 --- a/pyproject.toml +++ b/controller/pyproject.toml @@ -16,6 +16,7 @@ telnetlib3 = "^2.0.4" toml = "^0.10.2" smbus2 = "^0.5.0" aiohttp = "^3.12.13" +multidict = "^6.6.4" [build-system] diff --git a/tse_serial/tse_serial_controler.py b/controller/tse_serial/tse_serial_controler.py similarity index 87% rename from tse_serial/tse_serial_controler.py rename to controller/tse_serial/tse_serial_controler.py index e27cff2..c6dc014 100644 --- a/tse_serial/tse_serial_controler.py +++ b/controller/tse_serial/tse_serial_controler.py @@ -4,14 +4,17 @@ import aioserial import aiomqtt from tse_serial_interpreter import * from dataclasses import dataclass -import os +import os import sys -import toml +import toml import serial.tools.list_ports -#GLOBALS - -room = 'undefined' #TODO make be do get fronm file of configuration +# GLOBALS +room: str +platnoBckgdMoving = { + 'main': False, + 'side': False, +} # serPath = '/dev/serial/by-id/' # devLst = os.listdir(serPath) @@ -20,11 +23,16 @@ room = 'undefined' #TODO make be do get fronm file of configuration # serDev = devLst[0] portList = serial.tools.list_ports.comports() -if len(portList) < 1: - sys.exit("No serial port found") -#TODO if multiple ports idk, shouldn't ever happen but still -(serport, serdesc, serhwid) = portList[0] #TODO CHECK FOR TTYUSB0 -#TODO if port provided from conf, set, otherwise autodetect magical thing just works +candidates = ('/dev/ttyUSB0', '/dev/ttyACM0', '/dev/ttyUSB1', '/dev/ttyACM1') +serport = '' +# Find first serial port that exists +for port in portList: + if port.device in candidates: + serport = port.device + break + else: + print(portList) + sys.exit("No serial port found") aser: aioserial.AioSerial = aioserial.AioSerial( port=serport, @@ -32,10 +40,8 @@ aser: aioserial.AioSerial = aioserial.AioSerial( parity=serial.PARITY_NONE, bytesize=serial.EIGHTBITS, stopbits=serial.STOPBITS_ONE - ) +) - -#TODO get this from file da ni hardcoded? # altho itak je ta script za tist specific tse box in so vsi isti mapping_toggles = { "master": 1, @@ -63,10 +69,10 @@ reverse_lookup = { 1: ("power", "master", ""), 2: ("power", "audio", ""), 3: ("power", "projectors", ""), - + 5: ("platno", "main", "MOVING_DOWN"), 6: ("platno", "main", "MOVING_UP"), - + 7: ("platno", "side", "MOVING_DOWN"), 8: ("platno", "side", "MOVING_UP"), @@ -113,7 +119,7 @@ async def executeAndPublish(mqttClient, pubTopic, pubPayload, relStat): await mqttClient.publish(pubTopic, payload=pubPayload) await asyncio.sleep(0.1) #TODO probably remove - + async def handleTsePower(client, sysSelect, cmd): rel = RelayState(mapping_toggles[sysSelect], cmd == '1') await executeAndPublish(client, f'{room}/power/{sysSelect}/status', cmd, rel) @@ -129,13 +135,8 @@ async def handleTseSencilo(client, cmd): await executeAndPublish(client, topicPub, "MOVING_DOWN", rel) else: await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['UP'], False)) - await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['DOWN'], False)) - + await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['DOWN'], False)) -platnoBckgdMoving = { - 'main': False, - 'side': False, -} # mucho importante variable prav zares dedoles async def platnoTimeout(mqttClient, pubTopic, pubPayload, relStat: RelayState, intent, select): global platnoBckgdMoving @@ -144,12 +145,10 @@ async def platnoTimeout(mqttClient, pubTopic, pubPayload, relStat: RelayState, i await executeAndPublish(mqttClient, pubTopic, intent, relStat) platnoBckgdMoving[select] = False #TODO properly document why this is here and what it does + async def handleTsePlatno(client, proj, cmdType, cmd): global platnoBckgdMoving - #topicSplit = tval.split('/') - # {room} {projectors} {[select]} {platno} {move/goto} - #projector = topicSplit[2] - #command = topicSplit[4] + pubTop = f'{room}/projectors/{proj}/platno/status' if not (proj == "main" or proj == "side"): return @@ -203,21 +202,13 @@ async def handleTsePlatno(client, proj, cmdType, cmd): print('unknown command') async def task_command2serial(controlClient: aiomqtt.Client): - #print('oge') - await controlClient.subscribe(f"{room}/#") - #print('ogee') - #async with controlClient.messages as msgs: - async for mesg in controlClient.messages: - #print('oge') - #print(mesg, mesg.topic) - #mesg: aiomqtt.Message + async for mesg in controlClient.messages: topicVal = mesg.topic.value msgTopic = mesg.topic.value.split('/') cmnd = mesg.payload.decode() - #print('Received on: ', topicVal, ' with:', cmnd) - #print('bfr') + if mesg.topic.matches(f'{room}/projectors/+/platno/move') or mesg.topic.matches(f'{room}/projectors/+/platno/goto'): proj = msgTopic[-3] cmdType = msgTopic[-1] # move / goto @@ -236,7 +227,7 @@ async def task_command2serial(controlClient: aiomqtt.Client): await handleTseSencilo(controlClient, cmnd) else: - continue + continue # code after if block doesnt execute in this case #print("after") await asyncio.sleep(0.01) #TODO do we need this? (probably) @@ -244,12 +235,17 @@ async def task_command2serial(controlClient: aiomqtt.Client): async def main(): global room - conf = toml.load('./malinaConfig.toml') + + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) room = conf['global']['room'] - async with aiomqtt.Client('localhost', 1883) as client: #TODO omehčaj kodiranje + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: task_status = asyncio.create_task(task_status2mqtt(client)) task_control = asyncio.create_task(task_command2serial(client)) await asyncio.gather(task_status, task_control) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/tse_serial/tse_serial_interpreter.py b/controller/tse_serial/tse_serial_interpreter.py similarity index 100% rename from tse_serial/tse_serial_interpreter.py rename to controller/tse_serial/tse_serial_interpreter.py diff --git a/docs/Lučke.md b/docs/Lučke.md new file mode 100644 index 0000000..7d64ba5 --- /dev/null +++ b/docs/Lučke.md @@ -0,0 +1,15 @@ +Lučke v velikih predavalnicah +=== + +## Mapping po predavalnicah + +### P01 + + - 1 - Neonke tabla + - 2 - Neonke sredina + - 3 - Neonke vrata + - 4 - Okrogle 2 (vse razen ene stropne luči zgoraj) + - 5 - Neonke začetek + - 6 - Reflektorji 2 (ne dela) + - 7 - Okrogle 1 (ena stropna luč zgoraj) + - 8 - Stopnice diff --git a/eth0.notwork b/eth0.notwork deleted file mode 100644 index 7f7c756..0000000 --- a/eth0.notwork +++ /dev/null @@ -1,8 +0,0 @@ -[Match] - -Name=eth0 - -[Network] -Address=192.168.192.42 -Gateway=192.168.192.1 - diff --git a/extron_audio.service b/extron_audio.service deleted file mode 100644 index fb6ae80..0000000 --- a/extron_audio.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Extron audio matrix control -After=multi-user.target - -[Service] -ExecStart=/usr/bin/python3 /home/rpi/extron_audio_matrix_telnet_control.py -Type=simple -Restart=always -User=kat -Group=kat - -[Install] -WantedBy=multi-user.target diff --git a/frontend/vju_display/.env.development b/frontend/.env.development similarity index 100% rename from frontend/vju_display/.env.development rename to frontend/.env.development diff --git a/frontend/vju_display/.gitignore b/frontend/.gitignore similarity index 100% rename from frontend/vju_display/.gitignore rename to frontend/.gitignore diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..94a63a9 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,17 @@ +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` diff --git a/frontend/vju_display/env.d.ts b/frontend/env.d.ts similarity index 100% rename from frontend/vju_display/env.d.ts rename to frontend/env.d.ts diff --git a/frontend/vju_display/index.html b/frontend/index.html similarity index 90% rename from frontend/vju_display/index.html rename to frontend/index.html index a888544..3647951 100644 --- a/frontend/vju_display/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite App + MMM krmilnik
diff --git a/frontend/vju_display/package-lock.json b/frontend/package-lock.json similarity index 100% rename from frontend/vju_display/package-lock.json rename to frontend/package-lock.json diff --git a/frontend/vju_display/package.json b/frontend/package.json similarity index 100% rename from frontend/vju_display/package.json rename to frontend/package.json diff --git a/frontend/vju_display/src/App.vue b/frontend/src/App.vue similarity index 85% rename from frontend/vju_display/src/App.vue rename to frontend/src/App.vue index 32334e9..d69ac95 100644 --- a/frontend/vju_display/src/App.vue +++ b/frontend/src/App.vue @@ -14,19 +14,10 @@ document.addEventListener('contextmenu', event => event.preventDefault()); let urlParams = new URLSearchParams(window.location.search); - -const currentRoom = ref(urlParams.get('room') || 'none') // if no param specified - -const debugFlag = ref(urlParams.get('debug') == 'true'); - -// should also check if valid room parameter ampak se ne mudi -// TODO does this make sense al se naj naprej defaulta kr na p01? - +const currentRoom = ref(urlParams.get('room') || 'none') const pageNum = ref(0) - - const mqttStat = ref($mqtt.status()) watch(mqttStat, (_, newState) => { @@ -47,7 +38,7 @@ watch(pageNum, (_, newState) => { diff --git a/frontend/src/components/Lift.vue b/frontend/src/components/Lift.vue new file mode 100644 index 0000000..9b3951c --- /dev/null +++ b/frontend/src/components/Lift.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/frontend/src/components/LightControl.vue b/frontend/src/components/LightControl.vue new file mode 100644 index 0000000..5de361c --- /dev/null +++ b/frontend/src/components/LightControl.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/frontend/vju_display/src/components/MasterPowerControlModule.vue b/frontend/src/components/MasterPowerControlModule.vue similarity index 100% rename from frontend/vju_display/src/components/MasterPowerControlModule.vue rename to frontend/src/components/MasterPowerControlModule.vue diff --git a/frontend/vju_display/src/components/Numpad.vue b/frontend/src/components/Numpad.vue similarity index 92% rename from frontend/vju_display/src/components/Numpad.vue rename to frontend/src/components/Numpad.vue index 285d427..d9cdad2 100644 --- a/frontend/vju_display/src/components/Numpad.vue +++ b/frontend/src/components/Numpad.vue @@ -4,6 +4,7 @@ import {ref, computed} from 'vue' const emit = defineEmits(['submitPasscode']) defineProps([]) +// TODO: unhardocde this shit const correctCode = '1337' const passcodeLength = correctCode.length const enteredCode = ref('') @@ -45,14 +46,13 @@ const submit = () => {
Access Denied - + {{ enteredCode[i] ? '•' : ' ' }}
-
@@ -77,10 +77,6 @@ const submit = () => {
- - - -
@@ -89,19 +85,17 @@ const submit = () => { button { margin: 0.1em; flex: max-content; - padding: 1.5em; + padding: 1em; text-align: center; align-self: inherit; justify-content: space-evenly; - } .keypad { display: flex; flex-direction: column; - margin: 2em; + margin: 1.5em; align-content: space-evenly; - } .keypadRow { @@ -115,11 +109,15 @@ button { .keypadFeedback { display: inline-block; text-align: center; - //border: 2px solid darkgray; background-color: lightgray; background-clip: border-box; font-size: 2em; min-height: 2em; margin-bottom: 1em; + + border: 1px solid #000000; + border-radius: 3px; + background: #ffffff; + box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; } diff --git a/frontend/vju_display/src/components/Platno.vue b/frontend/src/components/Platno.vue similarity index 100% rename from frontend/vju_display/src/components/Platno.vue rename to frontend/src/components/Platno.vue diff --git a/frontend/vju_display/src/components/ProjectorPowerModule.vue b/frontend/src/components/ProjectorPowerModule.vue similarity index 100% rename from frontend/vju_display/src/components/ProjectorPowerModule.vue rename to frontend/src/components/ProjectorPowerModule.vue diff --git a/frontend/src/components/ProjectorShutter.vue b/frontend/src/components/ProjectorShutter.vue new file mode 100644 index 0000000..3cbfdcb --- /dev/null +++ b/frontend/src/components/ProjectorShutter.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/frontend/vju_display/src/components/Projektor.vue b/frontend/src/components/Projektor.vue similarity index 89% rename from frontend/vju_display/src/components/Projektor.vue rename to frontend/src/components/Projektor.vue index bb24a36..add1666 100644 --- a/frontend/vju_display/src/components/Projektor.vue +++ b/frontend/src/components/Projektor.vue @@ -42,6 +42,8 @@ function handleIncomingMQTT(topic: string, msg: string) { function handleProjectorStatus(typ: string, msg: string) { console.log('handling status') //console.log(projStatus) + console.debug(props.room, projStatus.power, projStatus.shutter) + if (typ == 'power') { projStatus.power = msg == '1' } else if (typ == 'shutter') { projStatus.shutter = msg == '1' } else if (typ == 'freeze') { projStatus.freeze = msg == '1' } @@ -62,7 +64,7 @@ function publishMQTTMsg(topic: string, msg: string) { -const publishPrefix = ref(props.room + '/projectors/' + props.position + '/command/') +const publishPrefix = ref(props.room + '/projectors/' + props.position + '/set/') //TODO organize better, binds, etc. @@ -80,18 +82,18 @@ const projStatus = reactive({

Power

-

Shutter

-

Freeze image

-
diff --git a/frontend/vju_display/src/components/ResetButton.vue b/frontend/src/components/ResetButton.vue similarity index 100% rename from frontend/vju_display/src/components/ResetButton.vue rename to frontend/src/components/ResetButton.vue diff --git a/frontend/vju_display/src/components/icons/DownIcon.vue b/frontend/src/components/icons/DownIcon.vue similarity index 100% rename from frontend/vju_display/src/components/icons/DownIcon.vue rename to frontend/src/components/icons/DownIcon.vue diff --git a/frontend/vju_display/src/components/icons/UpIcon.vue b/frontend/src/components/icons/UpIcon.vue similarity index 100% rename from frontend/vju_display/src/components/icons/UpIcon.vue rename to frontend/src/components/icons/UpIcon.vue diff --git a/frontend/vju_display/src/components/pages/AudioPage.vue b/frontend/src/components/pages/AudioPage.vue similarity index 100% rename from frontend/vju_display/src/components/pages/AudioPage.vue rename to frontend/src/components/pages/AudioPage.vue diff --git a/frontend/vju_display/src/components/pages/LightingPage.vue b/frontend/src/components/pages/LightingPage.vue similarity index 68% rename from frontend/vju_display/src/components/pages/LightingPage.vue rename to frontend/src/components/pages/LightingPage.vue index d3f9dcd..1d439af 100644 --- a/frontend/vju_display/src/components/pages/LightingPage.vue +++ b/frontend/src/components/pages/LightingPage.vue @@ -5,6 +5,7 @@ import {ref, onMounted, reactive} from 'vue' import {$mqtt} from 'vue-paho-mqtt' import DownIcon from '../icons/DownIcon.vue'; import UpIcon from '../icons/UpIcon.vue'; +import LightControl from '../LightControl.vue'; const props = defineProps({ room: String, @@ -85,6 +86,45 @@ function publishMQTTMsg(topic: string, msg: string) { const publishPrefix = ref(props.room + '/') +// TODO: unhardcode +const showCustom = true; + +// TODO: un-hard-code this +const lights = [ + { + id: 1, + name: "Tabla", + dimmable: true, + }, { + id: 5, + name: "Začetek", + dimmable: true, + }, { + id: 2, + name: "Sredina", + dimmable: true, + }, { + id: 3, + name: "Vrh", + dimmable: true, + }, { + id: 4, + name: "Vhod 1", + dimmable: false, + }, { + id: 7, + name: "Vhod 2", + dimmable: false, + // }, { + // id: 6, + // name: "Reflektorji 2 (ne dela)", + // dimmable: false, + }, { + id: 8, + name: "Stopnice", + dimmable: false, + } +] @@ -92,30 +132,49 @@ const publishPrefix = ref(props.room + '/')
-
-

Razsvetljava

-
-
+
+
+ + +
+
-
+
+
+ + +
+
+ +
+ +

Senčila