Explorar o código

add simple http api using fastapi to control button states

Noah Vogt hai 2 días
pai
achega
47b0748a9e
Modificáronse 6 ficheiros con 84 adicións e 2 borrados
  1. 1 0
      .gitignore
  2. 74 0
      VulcanBoard.py
  3. 1 0
      config/classes.py
  4. 6 1
      config/load.py
  5. 2 0
      requirements.txt
  6. 0 1
      widget.py

+ 1 - 0
.gitignore

@@ -1 +1,2 @@
 */__pycache__/*
 */__pycache__/*
+__pycache__/*

+ 74 - 0
VulcanBoard.py

@@ -21,6 +21,8 @@ import sys
 import asyncio
 import asyncio
 import time
 import time
 import colorama
 import colorama
+import uvicorn
+from fastapi import FastAPI, HTTPException
 
 
 from kivy.app import App
 from kivy.app import App
 from kivy.uix.gridlayout import GridLayout
 from kivy.uix.gridlayout import GridLayout
@@ -128,6 +130,18 @@ class VulcanBoardApp(App):
                     self.button_grid[(row, col)] = btn
                     self.button_grid[(row, col)] = btn
                     layout.add_widget(btn)
                     layout.add_widget(btn)
 
 
+            self.api_app = FastAPI(title="VulcanBoard API")
+            self._setup_api_routes()
+
+            uvicorn_config = uvicorn.Config(
+                app=self.api_app,
+                host="0.0.0.0",
+                port=config.api_port,
+                log_level="info",
+            )
+            server = uvicorn.Server(uvicorn_config)
+            self.async_task(server.serve())
+
             return layout
             return layout
 
 
     def on_button_pressed_once(self, button, btn_instance):
     def on_button_pressed_once(self, button, btn_instance):
@@ -213,6 +227,66 @@ class VulcanBoardApp(App):
             state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
             state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
         )
         )
 
 
+    def update_button_state_from_api(
+        self, dt, row: int, col: int, state_id: int
+    ):
+        btn = self.button_grid.get((row, col))
+        btn_config = self.button_config_map.get((row, col))
+        if not btn or not btn_config:
+            return
+
+        states = btn_config.get("states", [])
+        state = get_state_from_id(states, state_id)
+
+        btn.text = state.get("txt", "")
+        btn.state_id = state_id
+        btn.background_color = get_color_from_hex(
+            state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
+        )
+        btn.color = get_color_from_hex(
+            state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
+        )
+
+    def _setup_api_routes(self):
+        @self.api_app.get("/get_states")
+        def get_states(x: int, y: int):
+            btn_config = self.button_config_map.get((x, y))
+            if not btn_config:
+                raise HTTPException(status_code=404, detail="Button not found")
+            return {"states": btn_config.get("states", [])}
+
+        @self.api_app.get("/get_current_state")
+        def get_current_state(x: int, y: int):
+            btn = self.button_grid.get((x, y))
+            if not btn:
+                raise HTTPException(status_code=404, detail="Button not found")
+            return {"state_id": getattr(btn, "state_id", DEFAULT_STATE_ID)}
+
+        @self.api_app.get("/set_state")
+        @self.api_app.post("/set_state")
+        def set_state(x: int, y: int, state: int):
+            btn_config = self.button_config_map.get((x, y))
+            btn = self.button_grid.get((x, y))
+            if not btn_config or not btn:
+                raise HTTPException(status_code=404, detail="Button not found")
+
+            states = btn_config.get("states", [])
+            valid_state = None
+            for s in states:
+                if s.get("id", DEFAULT_STATE_ID) == state:
+                    valid_state = s
+                    break
+            if not valid_state:
+                raise HTTPException(
+                    status_code=400,
+                    detail=f"State {state} not found for this button",
+                )
+
+            Clock.schedule_once(
+                lambda dt: self.update_button_state_from_api(dt, x, y, state)
+            )
+            return {"status": "success", "state_id": state}
+
 
 
 def start_asyncio_loop():
 def start_asyncio_loop():
     asyncio.set_event_loop(asyncio.new_event_loop())
     asyncio.set_event_loop(asyncio.new_event_loop())

+ 1 - 0
config/classes.py

@@ -28,3 +28,4 @@ class Config:
     window_pos_x: int
     window_pos_x: int
     window_pos_y: int
     window_pos_y: int
     use_auto_fullscreen_mode: bool
     use_auto_fullscreen_mode: bool
+    api_port: int

+ 6 - 1
config/load.py

@@ -45,6 +45,7 @@ class ConfigLoader:
         self.window_pos_x = 0
         self.window_pos_x = 0
         self.window_pos_y = 0
         self.window_pos_y = 0
         self.use_auto_fullscreen_mode = False
         self.use_auto_fullscreen_mode = False
+        self.api_port = 0
 
 
     def get_config(self) -> Config | str:
     def get_config(self) -> Config | str:
         try:
         try:
@@ -59,7 +60,10 @@ class ConfigLoader:
                 self.set_window_pos = yaml_config.get("set_window_pos", False)
                 self.set_window_pos = yaml_config.get("set_window_pos", False)
                 self.window_pos_x = yaml_config.get("window_pos_x", 0)
                 self.window_pos_x = yaml_config.get("window_pos_x", 0)
                 self.window_pos_y = yaml_config.get("window_pos_y", 0)
                 self.window_pos_y = yaml_config.get("window_pos_y", 0)
-                self.use_auto_fullscreen_mode = yaml_config.get("use_auto_fullscreen_mode", False)
+                self.use_auto_fullscreen_mode = yaml_config.get(
+                    "use_auto_fullscreen_mode", False
+                )
+                self.api_port = yaml_config.get("api_port", 8080)
                 return self.__interpret_config()
                 return self.__interpret_config()
         except (FileNotFoundError, PermissionError, IOError) as error:
         except (FileNotFoundError, PermissionError, IOError) as error:
             return f"Error: Could not access config file at {self.config_path}. Reason: {error}"
             return f"Error: Could not access config file at {self.config_path}. Reason: {error}"
@@ -82,6 +86,7 @@ class ConfigLoader:
             self.window_pos_x,
             self.window_pos_x,
             self.window_pos_y,
             self.window_pos_y,
             self.use_auto_fullscreen_mode,
             self.use_auto_fullscreen_mode,
+            self.api_port,
         )
         )
 
 
     def __validate_buttons(self) -> None:
     def __validate_buttons(self) -> None:

+ 2 - 0
requirements.txt

@@ -2,3 +2,5 @@ pyyaml
 kivy
 kivy
 colorama
 colorama
 termcolor
 termcolor
+fastapi
+uvicorn

+ 0 - 1
widget.py

@@ -18,7 +18,6 @@ kivy.require("1.9.0")
 # is pressed (or released after a click/touch).
 # is pressed (or released after a click/touch).
 from kivy.uix.button import Button
 from kivy.uix.button import Button
 
 
-
 # The GridLayout arranges children in a matrix.
 # The GridLayout arranges children in a matrix.
 # It takes the available space and
 # It takes the available space and
 # divides it into columns and rows,
 # divides it into columns and rows,