mirror of
https://github.com/AkitaEngineering/Akita-Meshtastic-Meshcore-Bridge.git
synced 2026-10-31 01:42:01 +01:00
feat: add async support for Meshcore Bridge with REST API and MQTT integration
This commit is contained in:
1 parent
3703f32f87
commit
4bacbbf111
8 files changed
+467
-11
No files matched your search
@@ -0,0 +1,52 @@
|
|||||||
|
# Akita Meshtastic Meshcore Bridge Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- Python 3.9+ (recommended: 3.11+)
|
||||||
|
- All hardware connected and serial port identified (e.g., COM8)
|
||||||
|
- MQTT broker (if using MQTT transport)
|
||||||
|
- (Optional) FastAPI/uvicorn for REST API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Clone the repository:
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/AkitaEngineering/Akita-Meshtastic-Meshcore-Bridge.git
|
||||||
|
cd Akita-Meshtastic-Meshcore-Bridge
|
||||||
|
```
|
||||||
|
2. Create and activate a virtual environment:
|
||||||
|
```sh
|
||||||
|
python -m venv .venv
|
||||||
|
# Windows:
|
||||||
|
.venv\Scripts\activate
|
||||||
|
# Linux/macOS:
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
3. Install dependencies:
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- Copy and edit `examples/config.ini.example` to `config.ini`.
|
||||||
|
- Set the correct serial port, baud rate, MQTT, and API settings as needed.
|
||||||
|
- Ensure the `SERIAL_PROTOCOL` matches your device (`raw_serial` or `json_newline`).
|
||||||
|
|
||||||
|
## Running the Bridge
|
||||||
|
```sh
|
||||||
|
python run_bridge_async.py
|
||||||
|
```
|
||||||
|
- The bridge will log to console by default. Adjust logging in `config.ini` if needed.
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
- Only one process can access the serial port at a time.
|
||||||
|
- To enable the REST API, set `api_enabled = true` in your config and ensure FastAPI/uvicorn are installed.
|
||||||
|
- For production, use a process manager (systemd, pm2, etc.) to keep the bridge running.
|
||||||
|
- Monitor logs for errors or disconnects.
|
||||||
|
- To update, pull the latest code and re-install requirements if needed.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
- **Serial port access denied:** Ensure no other process is using the port. Reboot if needed.
|
||||||
|
- **No messages/events:** Check device connection and config. Enable debug logging for more details.
|
||||||
|
- **Unhandled exceptions:** Review logs. All async tasks now have robust exception handling and will log errors.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
- See README.md for more details and contact info.
|
||||||
@@ -51,21 +51,36 @@ Copy `examples/config.ini.example` to `config.ini` and edit it.
|
|||||||
- **For REST API (Optional):**
|
- **For REST API (Optional):**
|
||||||
Set `API_ENABLED = True` and configure `API_HOST` and `API_PORT` to enable the monitoring API.
|
Set `API_ENABLED = True` and configure `API_HOST` and `API_PORT` to enable the monitoring API.
|
||||||
|
|
||||||
### Run
|
|
||||||
python run_bridge.py
|
### Run (Sync or Async)
|
||||||
|
|
||||||
|
- **Synchronous (legacy):**
|
||||||
|
python run_bridge.py
|
||||||
|
|
||||||
|
- **Async (recommended, for meshcore_py and async MQTT):**
|
||||||
|
python run_bridge_async.py
|
||||||
|
|
||||||
|
The async entry point supports:
|
||||||
|
- Async Meshcore integration (meshcore_py)
|
||||||
|
- Async MQTT (asyncio-mqtt)
|
||||||
|
- Async REST API (FastAPI, if enabled)
|
||||||
|
|
||||||
|
#### Async API Server
|
||||||
|
If `API_ENABLED = True` in your config, the async bridge will launch a FastAPI server for health, metrics, and control endpoints (see below).
|
||||||
|
|
||||||
|
|
||||||
### REST API Endpoints (if enabled)
|
### REST API Endpoints (if enabled)
|
||||||
- `GET /api/health` - Get health status of all components
|
Endpoints are available on the configured API host/port (default: http://127.0.0.1:8080):
|
||||||
- `GET /api/metrics` - Get detailed metrics and statistics
|
|
||||||
- `GET /api/status` - Get combined health and metrics
|
- `GET /api/health` — Health status of all components
|
||||||
- `GET /api/info` - Get bridge information
|
- `GET /api/metrics` — Detailed metrics and statistics
|
||||||
- `POST /api/control` - Control actions (e.g., reset metrics)
|
- `GET /api/status` — Combined health and metrics
|
||||||
|
- `GET /api/info` — Bridge information
|
||||||
|
- `POST /api/control` — Control actions (e.g., reset metrics)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```bash
|
curl http://localhost:8080/api/health
|
||||||
curl http://localhost:8080/api/health
|
curl http://localhost:8080/api/metrics
|
||||||
curl http://localhost:8080/api/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# ammb/api_async.py
|
||||||
|
"""
|
||||||
|
Async REST API for monitoring and controlling the bridge using FastAPI.
|
||||||
|
"""
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import logging
|
||||||
|
from ammb.metrics import get_metrics
|
||||||
|
from ammb.health import get_health_monitor
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
async def health():
|
||||||
|
health_monitor = get_health_monitor()
|
||||||
|
health_data = health_monitor.get_overall_health()
|
||||||
|
return JSONResponse(content=health_data)
|
||||||
|
|
||||||
|
@app.get("/api/metrics")
|
||||||
|
async def metrics():
|
||||||
|
metrics = get_metrics()
|
||||||
|
metrics_data = metrics.get_all_stats()
|
||||||
|
return JSONResponse(content=metrics_data)
|
||||||
|
|
||||||
|
@app.get("/api/status")
|
||||||
|
async def status():
|
||||||
|
health_monitor = get_health_monitor()
|
||||||
|
metrics = get_metrics()
|
||||||
|
status = {
|
||||||
|
"health": health_monitor.get_overall_health(),
|
||||||
|
"metrics": metrics.get_all_stats(),
|
||||||
|
}
|
||||||
|
return JSONResponse(content=status)
|
||||||
|
|
||||||
|
@app.get("/api/info")
|
||||||
|
async def info():
|
||||||
|
# This endpoint should be extended to include async bridge state if needed
|
||||||
|
return JSONResponse(content={
|
||||||
|
"name": "Akita Meshtastic Meshcore Bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"external_transport": "async",
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.post("/api/control")
|
||||||
|
async def control(request: Request):
|
||||||
|
data = await request.json()
|
||||||
|
action = data.get("action")
|
||||||
|
if action == "reset_metrics":
|
||||||
|
metrics = get_metrics()
|
||||||
|
metrics.reset()
|
||||||
|
return JSONResponse(content={"message": "Metrics reset"})
|
||||||
|
return JSONResponse(content={"error": f"Unknown action: {action}"}, status_code=400)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# ammb/bridge_async.py
|
||||||
|
"""
|
||||||
|
Async Bridge orchestrator using MeshcoreAsyncHandler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ammb.meshcore_async_handler import MeshcoreAsyncHandler
|
||||||
|
from ammb.mqtt_async_handler import MQTTAsyncHandler
|
||||||
|
from ammb.config_handler import BridgeConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncBridge:
|
||||||
|
def __init__(self, config: BridgeConfig):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.config = config
|
||||||
|
self.meshcore_handler: Optional[MeshcoreAsyncHandler] = None
|
||||||
|
self.mqtt_handler: Optional[MQTTAsyncHandler] = None
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self._running = True
|
||||||
|
try:
|
||||||
|
if self.config.external_transport == "serial":
|
||||||
|
self.meshcore_handler = MeshcoreAsyncHandler(
|
||||||
|
serial_port=self.config.serial_port,
|
||||||
|
baud=self.config.serial_baud or 115200,
|
||||||
|
debug=self.config.log_level == "DEBUG",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.meshcore_handler.run()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Unhandled exception in meshcore handler: {e}", exc_info=True)
|
||||||
|
elif self.config.external_transport == "mqtt":
|
||||||
|
self.mqtt_handler = MQTTAsyncHandler(
|
||||||
|
broker=self.config.mqtt_broker,
|
||||||
|
port=self.config.mqtt_port,
|
||||||
|
topic_in=self.config.mqtt_topic_in,
|
||||||
|
topic_out=self.config.mqtt_topic_out,
|
||||||
|
username=self.config.mqtt_username,
|
||||||
|
password=self.config.mqtt_password,
|
||||||
|
qos=self.config.mqtt_qos or 0,
|
||||||
|
retain=self.config.mqtt_retain_out or False,
|
||||||
|
tls=self.config.mqtt_tls_enabled or False,
|
||||||
|
tls_ca_certs=self.config.mqtt_tls_ca_certs,
|
||||||
|
tls_insecure=self.config.mqtt_tls_insecure or False,
|
||||||
|
client_id=self.config.mqtt_client_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.mqtt_handler.set_message_handler(self.handle_mqtt_message)
|
||||||
|
await self.mqtt_handler.connect()
|
||||||
|
await self.mqtt_handler.run()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Unhandled exception in MQTT handler: {e}", exc_info=True)
|
||||||
|
else:
|
||||||
|
self.logger.error("Unsupported external transport in async bridge.")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.logger.info("AsyncBridge received cancellation signal.")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.critical(f"Unhandled exception in AsyncBridge: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
await self.shutdown()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
self.logger.info("Shutting down AsyncBridge and handlers...")
|
||||||
|
self._running = False
|
||||||
|
if self.meshcore_handler:
|
||||||
|
await self.meshcore_handler.disconnect()
|
||||||
|
if self.mqtt_handler:
|
||||||
|
await self.mqtt_handler.disconnect()
|
||||||
|
self.logger.info("AsyncBridge shutdown complete.")
|
||||||
|
|
||||||
|
async def handle_incoming_message(self, event):
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Received message: {event.payload}")
|
||||||
|
# Add additional async message processing here
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in handle_incoming_message: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def handle_mqtt_message(self, data):
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Received MQTT message: {data}")
|
||||||
|
# Add additional async message processing here if needed
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in handle_mqtt_message: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def handle_mqtt_message(self, data):
|
||||||
|
self.logger.info(f"Received MQTT message: {data}")
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# ammb/meshcore_async_handler.py
|
||||||
|
"""
|
||||||
|
Async Meshcore handler using meshcore_py for serial communication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional, Callable
|
||||||
|
from meshcore import MeshCore, EventType
|
||||||
|
|
||||||
|
class MeshcoreAsyncHandler:
|
||||||
|
"""
|
||||||
|
Async handler for Meshcore devices using meshcore_py.
|
||||||
|
"""
|
||||||
|
def __init__(self, serial_port: str, baud: int = 115200, debug: bool = False):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.serial_port = serial_port
|
||||||
|
self.baud = baud
|
||||||
|
self.debug = debug
|
||||||
|
self.meshcore: Optional[MeshCore] = None
|
||||||
|
self._event_handlers: Dict[EventType, Callable] = {}
|
||||||
|
self._connected = asyncio.Event()
|
||||||
|
self._disconnect_requested = False
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self.logger.info(f"Connecting to Meshcore device on {self.serial_port}...")
|
||||||
|
self.meshcore = await MeshCore.create_serial(self.serial_port, self.baud, debug=self.debug)
|
||||||
|
self.meshcore.subscribe(EventType.CONNECTED, self._on_connected)
|
||||||
|
self.meshcore.subscribe(EventType.DISCONNECTED, self._on_disconnected)
|
||||||
|
self._connected.set()
|
||||||
|
self.logger.info("Meshcore device connected.")
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
self._disconnect_requested = True
|
||||||
|
if self.meshcore:
|
||||||
|
await self.meshcore.disconnect()
|
||||||
|
self._connected.clear()
|
||||||
|
self.logger.info("Meshcore device disconnected.")
|
||||||
|
|
||||||
|
async def send_message(self, contact_or_key: Any, message: str):
|
||||||
|
if not self.meshcore:
|
||||||
|
raise RuntimeError("Meshcore not connected.")
|
||||||
|
result = await self.meshcore.commands.send_msg(contact_or_key, message)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error("send_msg returned None (no response from Meshcore).")
|
||||||
|
return False
|
||||||
|
if result.type == EventType.ERROR:
|
||||||
|
self.logger.error(f"Error sending message: {result.payload}")
|
||||||
|
return False
|
||||||
|
self.logger.info("Message sent successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def subscribe(self, event_type: EventType, handler: Callable):
|
||||||
|
if not self.meshcore:
|
||||||
|
raise RuntimeError("Meshcore not connected.")
|
||||||
|
# Wrap handler for centralized error logging
|
||||||
|
def safe_handler(event):
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(handler):
|
||||||
|
asyncio.create_task(handler(event))
|
||||||
|
else:
|
||||||
|
handler(event)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in event handler for {event_type}: {e}", exc_info=True)
|
||||||
|
self.meshcore.subscribe(event_type, safe_handler)
|
||||||
|
self.logger.info(f"Subscribed to event: {event_type}")
|
||||||
|
|
||||||
|
async def get_contacts(self):
|
||||||
|
if not self.meshcore:
|
||||||
|
raise RuntimeError("Meshcore not connected.")
|
||||||
|
result = await self.meshcore.commands.get_contacts()
|
||||||
|
if result is None:
|
||||||
|
self.logger.error("get_contacts returned None (no response from Meshcore).")
|
||||||
|
return None
|
||||||
|
if result.type == EventType.ERROR:
|
||||||
|
self.logger.error(f"Error getting contacts: {result.payload}")
|
||||||
|
return None
|
||||||
|
return result.payload
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
# Only connect once per run, and wait for disconnect request
|
||||||
|
try:
|
||||||
|
await self.connect()
|
||||||
|
while not self._disconnect_requested:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Unhandled exception in MeshcoreAsyncHandler.run: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Ensure disconnect is called only once per run
|
||||||
|
await self.disconnect()
|
||||||
|
|
||||||
|
def _on_connected(self, event):
|
||||||
|
self.logger.info(f"Connected: {event.payload}")
|
||||||
|
|
||||||
|
def _on_disconnected(self, event):
|
||||||
|
self.logger.warning(f"Disconnected: {event.payload}")
|
||||||
|
self._connected.clear()
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# ammb/mqtt_async_handler.py
|
||||||
|
"""
|
||||||
|
Async MQTT handler using asyncio-mqtt for external network interface.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
from asyncio_mqtt import Client, MqttError
|
||||||
|
|
||||||
|
class MQTTAsyncHandler:
|
||||||
|
def __init__(self, broker: str, port: int, topic_in: str, topic_out: str, username: Optional[str] = None, password: Optional[str] = None, qos: int = 0, retain: bool = False, tls: bool = False, tls_ca_certs: Optional[str] = None, tls_insecure: bool = False, client_id: Optional[str] = None):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.broker = broker
|
||||||
|
self.port = port
|
||||||
|
self.topic_in = topic_in
|
||||||
|
self.topic_out = topic_out
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.qos = qos
|
||||||
|
self.retain = retain
|
||||||
|
self.tls = tls
|
||||||
|
self.tls_ca_certs = tls_ca_certs
|
||||||
|
self.tls_insecure = tls_insecure
|
||||||
|
self.client_id = client_id
|
||||||
|
self._client: Optional[Client] = None
|
||||||
|
self._message_handler: Optional[Callable[[dict], None]] = None
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self._client = Client(self.broker, port=self.port, username=self.username, password=self.password, client_id=self.client_id)
|
||||||
|
if self.tls:
|
||||||
|
tls_params = {}
|
||||||
|
if self.tls_ca_certs:
|
||||||
|
tls_params['ca_certs'] = self.tls_ca_certs
|
||||||
|
if self.tls_insecure:
|
||||||
|
tls_params['cert_reqs'] = False
|
||||||
|
self._client.tls_set(**tls_params)
|
||||||
|
await self._client.connect()
|
||||||
|
self.logger.info(f"Connected to MQTT broker {self.broker}:{self.port}")
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
if self._client:
|
||||||
|
await self._client.disconnect()
|
||||||
|
self.logger.info("Disconnected from MQTT broker.")
|
||||||
|
|
||||||
|
async def publish(self, message: dict):
|
||||||
|
if not self._client:
|
||||||
|
raise RuntimeError("MQTT client not connected.")
|
||||||
|
payload = json.dumps(message)
|
||||||
|
await self._client.publish(self.topic_out, payload, qos=self.qos, retain=self.retain)
|
||||||
|
self.logger.info(f"Published message to {self.topic_out}")
|
||||||
|
|
||||||
|
def set_message_handler(self, handler: Callable[[dict], None]):
|
||||||
|
self._message_handler = handler
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
if not self._client:
|
||||||
|
await self.connect()
|
||||||
|
self._running = True
|
||||||
|
async with self._client.unfiltered_messages() as messages:
|
||||||
|
await self._client.subscribe(self.topic_in, qos=self.qos)
|
||||||
|
self.logger.info(f"Subscribed to {self.topic_in}")
|
||||||
|
async for msg in messages:
|
||||||
|
try:
|
||||||
|
payload = msg.payload.decode('utf-8')
|
||||||
|
data = json.loads(payload)
|
||||||
|
if self._message_handler:
|
||||||
|
self._message_handler(data)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error processing MQTT message: {e}")
|
||||||
|
if not self._running:
|
||||||
|
break
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
# Runtime dependencies for the Akita Meshtastic-Meshcore Bridge
|
# Runtime dependencies for the Akita Meshtastic-Meshcore Bridge
|
||||||
|
|
||||||
meshtastic>=2.2.0,<3.0.0
|
meshtastic>=2.2.0,<3.0.0
|
||||||
|
meshcore>=2.2.0
|
||||||
|
asyncio-mqtt>=0.14.0
|
||||||
|
fastapi>=0.110.0
|
||||||
|
uvicorn>=0.27.0
|
||||||
pyserial>=3.5,<4.0.0
|
pyserial>=3.5,<4.0.0
|
||||||
pypubsub>=4.0.0,<5.0.0
|
pypubsub>=4.0.0,<5.0.0
|
||||||
paho-mqtt>=1.6.0,<2.0.0
|
paho-mqtt>=1.6.0,<2.0.0
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# run_bridge_async.py
|
||||||
|
"""
|
||||||
|
Async entry point for Akita Meshtastic Meshcore Bridge using meshcore_py.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ammb.config_handler import CONFIG_FILE, load_config
|
||||||
|
from ammb.utils import setup_logging
|
||||||
|
from ammb.bridge_async import AsyncBridge
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"ERROR: Failed to import AMMB modules: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
logging.info("--- Akita Meshtastic Meshcore Bridge (Async) Starting ---")
|
||||||
|
|
||||||
|
config_path = os.path.join(project_root, CONFIG_FILE)
|
||||||
|
logging.info(f"Loading configuration from: {config_path}")
|
||||||
|
config = load_config(config_path)
|
||||||
|
if not config:
|
||||||
|
logging.critical("Failed to load configuration. Bridge cannot start.")
|
||||||
|
sys.exit(1)
|
||||||
|
logging.info("Configuration loaded successfully.")
|
||||||
|
logging.info(f"Selected external transport: {config.external_transport}")
|
||||||
|
|
||||||
|
setup_logging(config.log_level)
|
||||||
|
logging.debug(f"Logging level set to {config.log_level}")
|
||||||
|
|
||||||
|
bridge = AsyncBridge(config)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
import uvicorn
|
||||||
|
from multiprocessing import Process
|
||||||
|
def run_api():
|
||||||
|
uvicorn.run("ammb.api_async:app", host=config.api_host or "127.0.0.1", port=int(config.api_port or 8080), log_level="info")
|
||||||
|
|
||||||
|
api_proc = None
|
||||||
|
if getattr(config, "api_enabled", False):
|
||||||
|
api_proc = Process(target=run_api)
|
||||||
|
api_proc.start()
|
||||||
|
logging.info(f"Async API server started on http://{config.api_host or '127.0.0.1'}:{config.api_port or 8080}")
|
||||||
|
try:
|
||||||
|
await bridge.start()
|
||||||
|
finally:
|
||||||
|
if api_proc:
|
||||||
|
api_proc.terminate()
|
||||||
|
api_proc.join()
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info("KeyboardInterrupt received. Initiating graceful shutdown...")
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(f"Unhandled critical exception in async bridge execution: {e}", exc_info=True)
|
||||||
|
sys.exit(1)
|
||||||
|
logging.info("--- Akita Meshtastic Meshcore Bridge (Async) Stopped ---")
|
||||||
|
sys.exit(0)
|
||||||
Reference in new issue
Block a user