mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-06 16:53:21 +02:00
feat(channels): backend resend endpoint via CMD_SEND_RAW_PACKET
PR #3 of 5. Adds POST /api/messages/<msg_id>/resend, which re-broadcasts an own channel message verbatim using the raw_packet bytes captured at send time. Pushes the wire bytes directly through companion command 0x41 (CMD_SEND_RAW_PACKET), bypassing the higher-level send paths so repeaters dedupe by packet hash via Mesh::hasSeen — only previously-unreached nodes will pick up the resend. Returns 404 for unknown msg_id, 400 for not-own / missing snapshot / disconnected device, 500 for unexpected device errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1729,6 +1729,59 @@ class DeviceManager:
|
||||
logger.error(f"Failed to send channel message: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
# CMD_SEND_RAW_PACKET (firmware MyMesh.cpp:1976) — companion command 0x41.
|
||||
# Frame: [cmd=0x41, priority(1), raw_packet_bytes...]. Firmware parses
|
||||
# the packet via Packet::readFrom and queues it through sendPacket(),
|
||||
# bypassing the higher-level sendFlood path (no MSG_SENT event, just OK/ERR).
|
||||
_CMD_SEND_RAW_PACKET = 0x41
|
||||
|
||||
def resend_channel_message(self, msg_id: int) -> Dict:
|
||||
"""Re-broadcast an own channel message verbatim so repeaters can dedupe.
|
||||
|
||||
Looks up channel_messages.raw_packet (captured at send time, refreshed
|
||||
from echo correlation when clock drift is detected) and pushes the
|
||||
full wire bytes through CMD_SEND_RAW_PACKET. Repeaters that already
|
||||
forwarded the original packet ignore it via Mesh::hasSeen; repeaters
|
||||
that missed it can now pick it up — so the only new echoes we see are
|
||||
from previously-unreached nodes.
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return {'success': False, 'error': 'Device not connected'}
|
||||
|
||||
msg = self.db.get_channel_message_by_id(msg_id)
|
||||
if not msg:
|
||||
return {'success': False, 'error': f'Message #{msg_id} not found'}
|
||||
if not msg.get('is_own'):
|
||||
return {'success': False, 'error': 'Can only resend own messages'}
|
||||
raw_packet_hex = msg.get('raw_packet')
|
||||
if not raw_packet_hex:
|
||||
return {'success': False,
|
||||
'error': 'Message has no raw_packet snapshot (likely sent before this feature was deployed)'}
|
||||
|
||||
try:
|
||||
raw_packet = bytes.fromhex(raw_packet_hex)
|
||||
except ValueError as e:
|
||||
return {'success': False, 'error': f'Corrupt raw_packet: {e}'}
|
||||
|
||||
cmd_frame = bytes([self._CMD_SEND_RAW_PACKET, 0]) + raw_packet # priority 0
|
||||
try:
|
||||
from meshcore.events import EventType
|
||||
event = self.execute(
|
||||
self.mc.commands.send(cmd_frame, [EventType.OK, EventType.ERROR])
|
||||
)
|
||||
if event is None:
|
||||
return {'success': False, 'error': 'No response from device'}
|
||||
if event.type == EventType.ERROR:
|
||||
err = getattr(event, 'payload', {}).get('reason') or \
|
||||
getattr(event, 'payload', {}).get('error') or 'unknown error'
|
||||
logger.warning(f"Resend msg #{msg_id} failed: {err}")
|
||||
return {'success': False, 'error': f'Device rejected resend: {err}'}
|
||||
logger.info(f"Resent channel msg #{msg_id} via CMD_SEND_RAW_PACKET ({len(raw_packet)} bytes)")
|
||||
return {'success': True, 'message': 'Resent', 'id': msg_id, 'bytes': len(raw_packet)}
|
||||
except Exception as e:
|
||||
logger.error(f"resend_channel_message #{msg_id} failed: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def send_dm(self, recipient_pubkey: str, text: str) -> Dict:
|
||||
"""Send a direct message with background retry. Returns result dict."""
|
||||
if not self.is_connected:
|
||||
|
||||
@@ -73,6 +73,16 @@ def send_message(text: str, reply_to: Optional[str] = None, channel_index: int =
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
def resend_channel_message(msg_id: int) -> Dict:
|
||||
"""Re-broadcast an own channel message verbatim (raw resend with same packet hash)."""
|
||||
try:
|
||||
dm = _get_dm()
|
||||
return dm.resend_channel_message(msg_id)
|
||||
except Exception as e:
|
||||
logger.error(f"resend_channel_message error: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Contacts
|
||||
# =============================================================================
|
||||
|
||||
@@ -608,6 +608,32 @@ def get_message_meta(msg_id):
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/messages/<int:msg_id>/resend', methods=['POST'])
|
||||
def resend_channel_message(msg_id):
|
||||
"""Raw re-broadcast of an own channel message via CMD_SEND_RAW_PACKET.
|
||||
|
||||
Pushes the exact stored wire bytes again so repeaters that already saw
|
||||
the original packet dedupe it (same Mesh::hasSeen hash), while any
|
||||
repeaters that missed it can pick it up. Used for "I never heard echoes
|
||||
back" and "I want better coverage" scenarios.
|
||||
"""
|
||||
try:
|
||||
result = cli.resend_channel_message(msg_id)
|
||||
if result.get('success'):
|
||||
return jsonify(result), 200
|
||||
err = result.get('error', 'Resend failed')
|
||||
# 404 for missing snapshot or unknown id, 400 for ownership/disconnect,
|
||||
# 500 for unexpected device errors.
|
||||
if 'not found' in err.lower():
|
||||
return jsonify(result), 404
|
||||
if 'no raw_packet' in err.lower() or 'own messages' in err.lower() or 'not connected' in err.lower():
|
||||
return jsonify(result), 400
|
||||
return jsonify(result), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error resending message #{msg_id}: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/messages', methods=['POST'])
|
||||
def send_message():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user