mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
import logging
|
|
from hashlib import sha256
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from meshcore import EventType
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.dependencies import require_connected
|
|
from app.models import Channel
|
|
from app.radio import radio_manager
|
|
from app.radio_sync import upsert_channel_from_radio_slot
|
|
from app.repository import ChannelRepository
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/channels", tags=["channels"])
|
|
|
|
|
|
class CreateChannelRequest(BaseModel):
|
|
name: str = Field(min_length=1, max_length=32)
|
|
key: str | None = Field(
|
|
default=None,
|
|
description="Channel key as hex string (32 chars = 16 bytes). If omitted or name starts with #, key is derived from name hash.",
|
|
)
|
|
|
|
|
|
@router.get("", response_model=list[Channel])
|
|
async def list_channels() -> list[Channel]:
|
|
"""List all channels from the database."""
|
|
return await ChannelRepository.get_all()
|
|
|
|
|
|
@router.get("/{key}", response_model=Channel)
|
|
async def get_channel(key: str) -> Channel:
|
|
"""Get a specific channel by key (32-char hex string)."""
|
|
channel = await ChannelRepository.get_by_key(key)
|
|
if not channel:
|
|
raise HTTPException(status_code=404, detail="Channel not found")
|
|
return channel
|
|
|
|
|
|
@router.post("", response_model=Channel)
|
|
async def create_channel(request: CreateChannelRequest) -> Channel:
|
|
"""Create a channel in the database.
|
|
|
|
Channels are NOT pushed to radio on creation. They are loaded to the radio
|
|
automatically when sending a message (see messages.py send_channel_message).
|
|
"""
|
|
is_hashtag = request.name.startswith("#")
|
|
|
|
# Determine the channel secret
|
|
if request.key and not is_hashtag:
|
|
try:
|
|
key_bytes = bytes.fromhex(request.key)
|
|
if len(key_bytes) != 16:
|
|
raise HTTPException(
|
|
status_code=400, detail="Channel key must be exactly 16 bytes (32 hex chars)"
|
|
)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid hex string for key") from None
|
|
else:
|
|
# Derive key from name hash (same as meshcore library does)
|
|
key_bytes = sha256(request.name.encode("utf-8")).digest()[:16]
|
|
|
|
key_hex = key_bytes.hex().upper()
|
|
logger.info("Creating channel %s: %s (hashtag=%s)", key_hex, request.name, is_hashtag)
|
|
|
|
# Store in database only - radio sync happens at send time
|
|
await ChannelRepository.upsert(
|
|
key=key_hex,
|
|
name=request.name,
|
|
is_hashtag=is_hashtag,
|
|
on_radio=False,
|
|
)
|
|
|
|
return Channel(
|
|
key=key_hex,
|
|
name=request.name,
|
|
is_hashtag=is_hashtag,
|
|
on_radio=False,
|
|
)
|
|
|
|
|
|
@router.post("/sync")
|
|
async def sync_channels_from_radio(max_channels: int = Query(default=40, ge=1, le=40)) -> dict:
|
|
"""Sync channels from the radio to the database."""
|
|
require_connected()
|
|
|
|
logger.info("Syncing channels from radio (checking %d slots)", max_channels)
|
|
count = 0
|
|
|
|
async with radio_manager.radio_operation("sync_channels_from_radio") as mc:
|
|
for idx in range(max_channels):
|
|
result = await mc.commands.get_channel(idx)
|
|
|
|
if result.type == EventType.CHANNEL_INFO:
|
|
key_hex = await upsert_channel_from_radio_slot(result.payload, on_radio=True)
|
|
if key_hex is not None:
|
|
count += 1
|
|
logger.debug(
|
|
"Synced channel %s: %s", key_hex, result.payload.get("channel_name")
|
|
)
|
|
|
|
logger.info("Synced %d channels from radio", count)
|
|
return {"synced": count}
|
|
|
|
|
|
@router.post("/{key}/mark-read")
|
|
async def mark_channel_read(key: str) -> dict:
|
|
"""Mark a channel as read (update last_read_at timestamp)."""
|
|
channel = await ChannelRepository.get_by_key(key)
|
|
if not channel:
|
|
raise HTTPException(status_code=404, detail="Channel not found")
|
|
|
|
updated = await ChannelRepository.update_last_read_at(key)
|
|
if not updated:
|
|
raise HTTPException(status_code=500, detail="Failed to update read state")
|
|
|
|
return {"status": "ok", "key": channel.key}
|
|
|
|
|
|
@router.delete("/{key}")
|
|
async def delete_channel(key: str) -> dict:
|
|
"""Delete a channel from the database by key.
|
|
|
|
Note: This does not clear the channel from the radio. The radio's channel
|
|
slots are managed separately (channels are loaded temporarily when sending).
|
|
"""
|
|
logger.info("Deleting channel %s from database", key)
|
|
await ChannelRepository.delete(key)
|
|
return {"status": "ok"}
|