pre uart exchange
This commit is contained in:
53
tool/core/cmd/flash_info.py
Normal file
53
tool/core/cmd/flash_info.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# tool/core/cmd/flash_info.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class flash_info:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
|
||||
def get(self):
|
||||
import struct
|
||||
self.bus.send_request(COMMANDS['get_flash_info'])
|
||||
|
||||
data = self.bus.receive_response(length=21)
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
payload = data['data']
|
||||
ext_block_size = struct.unpack('<I', payload[0:4])[0]
|
||||
ext_total_blocks = struct.unpack('<I', payload[4:8])[0]
|
||||
ext_free_blocks = struct.unpack('<I', payload[8:12])[0]
|
||||
int_slot_size = struct.unpack('<I', payload[12:16])[0]
|
||||
ext_page_size = struct.unpack('<H', payload[16:18])[0]
|
||||
int_page_size = struct.unpack('<H', payload[18:20])[0]
|
||||
max_path_len = payload[20]
|
||||
|
||||
result = {
|
||||
'ext_block_size': ext_block_size,
|
||||
'ext_total_blocks': ext_total_blocks,
|
||||
'ext_free_blocks': ext_free_blocks,
|
||||
'int_slot_size': int_slot_size,
|
||||
'ext_page_size': ext_page_size,
|
||||
'int_page_size': int_page_size,
|
||||
'max_path_len': max_path_len,
|
||||
'ext_total_size': ext_block_size * ext_total_blocks,
|
||||
'ext_free_size': ext_block_size * ext_free_blocks,
|
||||
'ext_used_size': ext_block_size * (ext_total_blocks - ext_free_blocks)
|
||||
}
|
||||
return result
|
||||
|
||||
def print(self, result):
|
||||
if not result:
|
||||
return
|
||||
|
||||
console.print(f"[info]Flash-Informationen:[/info]")
|
||||
console.print(f" • [info]Externer Flash:[/info] {result['ext_total_size']/1024/1024:.2f} MB ({result['ext_total_blocks']} Blöcke à {result['ext_block_size']} Bytes)")
|
||||
console.print(f" - Belegt: {result['ext_used_size']/1024/1024:.2f} MB ({result['ext_total_blocks'] - result['ext_free_blocks']} Blöcke)")
|
||||
console.print(f" - Frei: {result['ext_free_size']/1024/1024:.2f} MB ({result['ext_free_blocks']} Blöcke)")
|
||||
console.print(f" • [info]FW Flash Slot:[/info] {result['int_slot_size']/1024:.2f} KB")
|
||||
console.print(f" • [info]EXTFLASH Seitengröße:[/info] {result['ext_page_size']} Bytes")
|
||||
console.print(f" • [info]INTFLASH Seitengröße:[/info] {result['int_page_size']} Bytes")
|
||||
console.print(f" • [info]Maximale Pfadlänge:[/info] {result['max_path_len']} Zeichen")
|
||||
53
tool/core/cmd/fw_status.py
Normal file
53
tool/core/cmd/fw_status.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# tool/core/cmd/fw_status.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class fw_status:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
|
||||
def get(self):
|
||||
import struct
|
||||
self.bus.send_request(COMMANDS['get_firmware_status'])
|
||||
|
||||
data = self.bus.receive_response(length=10)
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
header = data['data']
|
||||
status = header[0]
|
||||
app_version_raw = struct.unpack('<I', header[1:5])[0]
|
||||
ker_version_raw = struct.unpack('<I', header[5:9])[0]
|
||||
str_len = header[9]
|
||||
|
||||
fw_string_bytes = self.bus.connection.read(str_len)
|
||||
fw_string = fw_string_bytes.decode('utf-8')
|
||||
|
||||
result = {
|
||||
'status': status,
|
||||
'fw_version_raw': hex(app_version_raw),
|
||||
'kernel_version_raw': hex(ker_version_raw),
|
||||
'fw_major': (app_version_raw >> 24) & 0xFF,
|
||||
'fw_minor': (app_version_raw >> 16) & 0xFF,
|
||||
'fw_patch': (app_version_raw >> 8)& 0xFF,
|
||||
'kernel_major': (ker_version_raw >> 16) & 0xFF,
|
||||
'kernel_minor': (ker_version_raw >> 8) & 0xFF,
|
||||
'kernel_patch': ker_version_raw & 0xFF,
|
||||
'fw_string': fw_string,
|
||||
'kernel_string': f"{(ker_version_raw >> 16) & 0xFF}.{(ker_version_raw >> 8) & 0xFF}.{ker_version_raw & 0xFF}"
|
||||
}
|
||||
return result
|
||||
|
||||
def print(self, result):
|
||||
if not result:
|
||||
return
|
||||
|
||||
status = "UNKNOWN"
|
||||
if result['status'] == 0x00: status = "CONFIRMED"
|
||||
elif result['status'] == 0x01: status = "PENDING"
|
||||
elif result['status'] == 0x02: status = "TESTING"
|
||||
console.print(f"[info]Firmware Status[/info] des Controllers ist [info]{status}[/info]:")
|
||||
console.print(f" • Firmware: [info]{result['fw_string']}[/info] ({result['fw_major']}.{result['fw_minor']}.{result['fw_patch']})")
|
||||
console.print(f" • Kernel: [info]{result['kernel_string']}[/info] ({result['kernel_major']}.{result['kernel_minor']}.{result['kernel_patch']})")
|
||||
76
tool/core/cmd/get_file.py
Normal file
76
tool/core/cmd/get_file.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# tool/core/cmd/get_file.py
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS
|
||||
|
||||
class get_file:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self, source_path: str, dest_path: str):
|
||||
try:
|
||||
p = Path(dest_path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(p, 'wb') as f:
|
||||
pass
|
||||
except Exception as e:
|
||||
console_err.print(f"Fehler: Kann Zieldatei nicht anlegen: {e}")
|
||||
return None
|
||||
|
||||
source_path_bytes = source_path.encode('utf-8')
|
||||
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
|
||||
device_file_crc = None
|
||||
try:
|
||||
self.bus.send_request(COMMANDS['crc_32'], payload)
|
||||
crc_resp = self.bus.receive_response(length=4)
|
||||
if crc_resp and crc_resp.get('type') == 'response':
|
||||
device_file_crc = struct.unpack('<I', crc_resp['data'])[0]
|
||||
except Exception:
|
||||
device_file_crc = None
|
||||
|
||||
self.bus.send_request(COMMANDS['get_file'], payload)
|
||||
|
||||
stream_res = self.bus.receive_stream()
|
||||
|
||||
if not stream_res or stream_res.get('type') == 'error':
|
||||
return None
|
||||
|
||||
file_data = stream_res['data']
|
||||
remote_crc = stream_res['crc32']
|
||||
|
||||
if local_crc == remote_crc:
|
||||
with open(p, 'wb') as f:
|
||||
f.write(file_data)
|
||||
success = True
|
||||
else:
|
||||
with open(p, 'wb') as f:
|
||||
f.write(file_data)
|
||||
success = False
|
||||
|
||||
return {
|
||||
'success': success,
|
||||
'source_path': source_path,
|
||||
'dest_path': dest_path,
|
||||
'crc32_remote': remote_crc,
|
||||
'crc32_local': local_crc,
|
||||
'crc32_device_file': device_file_crc,
|
||||
'size': len(file_data)
|
||||
}
|
||||
|
||||
def print(self, result):
|
||||
if not result:
|
||||
return
|
||||
|
||||
if result['success']:
|
||||
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich heruntergeladen.")
|
||||
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
|
||||
else:
|
||||
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde nicht korrekt empfangen!")
|
||||
|
||||
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
|
||||
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
|
||||
if result.get('crc32_device_file') is not None:
|
||||
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]")
|
||||
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
|
||||
71
tool/core/cmd/list_dir.py
Normal file
71
tool/core/cmd/list_dir.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# tool/core/cmd/list_dir.py
|
||||
import struct
|
||||
from core.utils import console
|
||||
from core.protocol import COMMANDS
|
||||
|
||||
class list_dir:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self, path: str, recursive: bool = False):
|
||||
# Wir stellen sicher, dass der Pfad nicht leer ist und normalisieren ihn leicht
|
||||
clean_path = path if path == "/" else path.rstrip('/')
|
||||
path_bytes = clean_path.encode('utf-8')
|
||||
|
||||
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||
self.bus.send_request(COMMANDS['list_dir'], payload)
|
||||
|
||||
chunks = self.bus.receive_list()
|
||||
if chunks is None:
|
||||
return None
|
||||
|
||||
entries = []
|
||||
for chunk in chunks:
|
||||
if len(chunk) < 6: # Typ(1) + Size(4) + min 1 char Name
|
||||
continue
|
||||
|
||||
is_dir = chunk[0] == 1
|
||||
size = struct.unpack('<I', chunk[1:5])[0] if not is_dir else None
|
||||
name = chunk[5:].decode('utf-8').rstrip('\x00')
|
||||
|
||||
entry = {
|
||||
'name': name,
|
||||
'is_dir': is_dir,
|
||||
'size': size
|
||||
}
|
||||
|
||||
if recursive and is_dir:
|
||||
# Rekursiver Aufruf: Pfad sauber zusammenfügen
|
||||
sub_path = f"{clean_path}/{name}"
|
||||
entry['children'] = self.get(sub_path, recursive=True)
|
||||
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
def print(self, entries, path: str, prefix: str = ""):
|
||||
if prefix == "":
|
||||
console.print(f"Inhalt von [info]{path}[/info]:")
|
||||
|
||||
if not entries:
|
||||
return
|
||||
|
||||
# Sortierung: Verzeichnisse zuerst
|
||||
entries.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
# Prüfen, ob es das letzte Element auf dieser Ebene ist
|
||||
is_last = (i == len(entries) - 1)
|
||||
connector = "└" if is_last else "├"
|
||||
|
||||
icon = "📁" if entry['is_dir'] else "📄"
|
||||
size_str = f" ({entry['size']/1024:.2f} KB)" if entry['size'] is not None else ""
|
||||
|
||||
# Ausgabe der aktuellen Zeile
|
||||
console.print(f"{prefix}{connector}{icon} [info]{entry['name']}[/info]{size_str}")
|
||||
|
||||
# Wenn Kinder vorhanden sind, rekursiv weiter
|
||||
if 'children' in entry and entry['children']:
|
||||
# Für die Kinder-Ebene das Prefix anpassen
|
||||
extension = " " if is_last else "│ "
|
||||
self.print(entry['children'], "", prefix=prefix + extension)
|
||||
28
tool/core/cmd/proto.py
Normal file
28
tool/core/cmd/proto.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# tool/core/cmd/proto.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class proto:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self):
|
||||
self.bus.send_request(COMMANDS['get_protocol_version'], None)
|
||||
|
||||
data = self.bus.receive_response(length=1)
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
payload = data['data']
|
||||
result = {
|
||||
'protocol_version': payload[0]
|
||||
}
|
||||
return result
|
||||
|
||||
def print(self, result):
|
||||
if not result:
|
||||
return
|
||||
|
||||
protocol_version = result['protocol_version']
|
||||
console.print(f"[title]Protokoll Version[/info] des Controllers ist [info]{protocol_version}[/info]:")
|
||||
37
tool/core/cmd/rename.py
Normal file
37
tool/core/cmd/rename.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# tool/core/cmd/rename.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class rename:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self, source_path: str, dest_path: str):
|
||||
source_path_bytes = source_path.encode('utf-8')
|
||||
dest_path_bytes = dest_path.encode('utf-8')
|
||||
|
||||
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
|
||||
payload += struct.pack('B', len(dest_path_bytes)) + dest_path_bytes
|
||||
|
||||
self.bus.send_request(COMMANDS['rename'], payload)
|
||||
|
||||
data = self.bus.receive_ack()
|
||||
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
return {
|
||||
'success': data.get('type') == 'ack',
|
||||
'source_path': source_path,
|
||||
'dest_path': dest_path
|
||||
}
|
||||
|
||||
def print(self, result):
|
||||
if not result or not result.get('success'):
|
||||
return
|
||||
|
||||
console.print(
|
||||
f"Pfad [info]{result['source_path']}[/info] wurde erfolgreich in "
|
||||
f"[info]{result['dest_path']}[/info] umbenannt."
|
||||
)
|
||||
32
tool/core/cmd/rm.py
Normal file
32
tool/core/cmd/rm.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# tool/core/cmd/rm.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class rm:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self, path: str):
|
||||
path_bytes = path.encode('utf-8')
|
||||
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||
self.bus.send_request(COMMANDS['rm'], payload)
|
||||
|
||||
# 1 Byte Type + 4 Byte Size = 5
|
||||
data = self.bus.receive_ack()
|
||||
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
if data.get('type') == 'ack':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def print(self, result, path: str):
|
||||
if result is None:
|
||||
console_err.print(f"Fehler: Pfad [error]{path}[/error] konnte nicht entfernt werden.")
|
||||
elif result is False:
|
||||
console_err.print(f"Fehler: Pfad [error]{path}[/error] existiert nicht oder konnte nicht entfernt werden.")
|
||||
else:
|
||||
console.print(f"Pfad [info]{path}[/info] wurde erfolgreich entfernt.")
|
||||
36
tool/core/cmd/stat.py
Normal file
36
tool/core/cmd/stat.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# tool/core/cmd/stat.py
|
||||
import struct
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import COMMANDS, ERRORS
|
||||
|
||||
class stat:
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def get(self, path: str):
|
||||
path_bytes = path.encode('utf-8')
|
||||
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||
self.bus.send_request(COMMANDS['stat'], payload)
|
||||
|
||||
# 1 Byte Type + 4 Byte Size = 5
|
||||
data = self.bus.receive_response(length=5)
|
||||
|
||||
if not data or data.get('type') == 'error':
|
||||
return None
|
||||
|
||||
payload = data['data']
|
||||
result = {
|
||||
'is_directory': payload[0] == 1,
|
||||
'size': struct.unpack('<I', payload[1:5])[0]
|
||||
}
|
||||
return result
|
||||
|
||||
def print(self, result, path: str):
|
||||
if not result:
|
||||
return
|
||||
|
||||
t_name = "📁 Verzeichnis" if result['is_directory'] else "📄 Datei"
|
||||
console.print(f"[info_title]Stat[/info_title] für [info]{path}[/info]:")
|
||||
console.print(f" • Typ: [info]{t_name}[/info]")
|
||||
if not result['is_directory']:
|
||||
console.print(f" • Grösse: [info]{result['size']/1024:.2f} KB[/info] ({result['size']} Bytes)")
|
||||
48
tool/core/config.py
Normal file
48
tool/core/config.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# tool/core/config.py
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self._config = None
|
||||
self.config_name = "config.yaml"
|
||||
self.custom_path = None # Speicher für den Parameter-Pfad
|
||||
self.debug = False
|
||||
|
||||
def _load(self):
|
||||
if self._config is not None:
|
||||
return
|
||||
|
||||
from core.utils import console
|
||||
|
||||
search_paths = []
|
||||
if self.custom_path:
|
||||
search_paths.append(Path(self.custom_path))
|
||||
|
||||
search_paths.append(Path(os.getcwd()) / self.config_name)
|
||||
search_paths.append(Path(__file__).parent.parent / self.config_name)
|
||||
|
||||
config_path = None
|
||||
for p in search_paths:
|
||||
if p.exists():
|
||||
config_path = p
|
||||
break
|
||||
|
||||
if not config_path:
|
||||
raise FileNotFoundError(f"Konfiguration konnte an keinem Ort gefunden werden.")
|
||||
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"Konfiguration nicht gefunden: {self.config_name}")
|
||||
else:
|
||||
if self.debug: console.print(f"[bold green]✓[/bold green] Konfiguration geladen: [info]{config_path}[/info]")
|
||||
|
||||
with open(config_path, 'r') as f:
|
||||
self._config = yaml.safe_load(f)
|
||||
|
||||
@property
|
||||
def serial_settings(self):
|
||||
self._load()
|
||||
return self._config.get('serial', {})
|
||||
|
||||
cfg = Config()
|
||||
88
tool/core/protocol.py
Normal file
88
tool/core/protocol.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# tool/core/protocol.py
|
||||
VERSION = {
|
||||
"min_protocol_version": 1,
|
||||
"max_protocol_version": 1,
|
||||
"current_protocol_version": None
|
||||
}
|
||||
|
||||
SYNC_SEQ = b'BUZZ'
|
||||
|
||||
ERRORS = {
|
||||
0x00: "NONE",
|
||||
0x01: "INVALID_COMMAND",
|
||||
0x02: "INVALID_PARAMETERS",
|
||||
0x03: "MISSING_PARAMETERS",
|
||||
|
||||
0x10: "FILE_NOT_FOUND",
|
||||
0x11: "ALREADY_EXISTS",
|
||||
0x12: "NOT_A_DIRECTORY",
|
||||
0x13: "IS_A_DIRECTORY",
|
||||
0x14: "ACCESS_DENIED",
|
||||
0x15: "NO_SPACE",
|
||||
0x16: "FILE_TOO_LARGE",
|
||||
|
||||
0x20: "IO_ERROR",
|
||||
0x21: "TIMEOUT",
|
||||
0x22: "CRC_MISMATCH",
|
||||
0x23: "TRANSFER_ABORTED",
|
||||
|
||||
0x30: "NOT_SUPPORTED",
|
||||
0x31: "BUSY",
|
||||
0x32: "INTERNAL_ERROR",
|
||||
|
||||
0x40: "NOT_IMPLEMENTED",
|
||||
}
|
||||
|
||||
FRAME_TYPE_REQUEST = 0x01
|
||||
FRAME_TYPE_ACK = 0x10
|
||||
FRAME_TYPE_RESPONSE = 0x11
|
||||
FRAME_TYPE_STREAM_START = 0x12
|
||||
FRAME_TYPE_STREAM_CHUNK = 0x13
|
||||
FRAME_TYPE_STREAM_END = 0x14
|
||||
FRAME_TYPE_LIST_START = 0x15
|
||||
FRAME_TYPE_LIST_CHUNK = 0x16
|
||||
FRAME_TYPE_LIST_END = 0x17
|
||||
FRAME_TYPE_ERROR = 0xFF
|
||||
|
||||
FRAME_TYPES = {
|
||||
'request': FRAME_TYPE_REQUEST,
|
||||
'ack': FRAME_TYPE_ACK,
|
||||
'response': FRAME_TYPE_RESPONSE,
|
||||
'error': FRAME_TYPE_ERROR,
|
||||
'stream_start': FRAME_TYPE_STREAM_START,
|
||||
'stream_chunk': FRAME_TYPE_STREAM_CHUNK,
|
||||
'stream_end': FRAME_TYPE_STREAM_END,
|
||||
'list_start': FRAME_TYPE_LIST_START,
|
||||
'list_chunk': FRAME_TYPE_LIST_CHUNK,
|
||||
'list_end': FRAME_TYPE_LIST_END
|
||||
}
|
||||
|
||||
CMD_GET_PROTOCOL_VERSION = 0x00
|
||||
CMD_GET_FIRMWARE_STATUS = 0x01
|
||||
CMD_GET_FLASH_INFO = 0x02
|
||||
|
||||
CMD_LIST_DIR = 0x10
|
||||
CMD_CRC_32 = 0x11
|
||||
CMD_MKDIR = 0x12
|
||||
CMD_RM = 0x13
|
||||
CMD_STAT = 0x18
|
||||
CMD_RENAME = 0x19
|
||||
|
||||
CMD_PUT_FILE = 0x20
|
||||
CMD_PUT_FW = 0x21
|
||||
CMD_GET_FILE = 0x22
|
||||
|
||||
COMMANDS = {
|
||||
'get_protocol_version': CMD_GET_PROTOCOL_VERSION,
|
||||
'get_firmware_status': CMD_GET_FIRMWARE_STATUS,
|
||||
'get_flash_info': CMD_GET_FLASH_INFO,
|
||||
'list_dir': CMD_LIST_DIR,
|
||||
'stat': CMD_STAT,
|
||||
'rm': CMD_RM,
|
||||
'rename': CMD_RENAME,
|
||||
'mkdir': CMD_MKDIR,
|
||||
'crc_32': CMD_CRC_32,
|
||||
'put_file': CMD_PUT_FILE,
|
||||
'get_file': CMD_GET_FILE,
|
||||
'put_fw': CMD_PUT_FW,
|
||||
}
|
||||
279
tool/core/serial_conn.py
Normal file
279
tool/core/serial_conn.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# tool/core/serial_conn.py
|
||||
import struct
|
||||
import serial
|
||||
import time
|
||||
from core.utils import console, console_err
|
||||
from core.protocol import SYNC_SEQ, ERRORS, FRAME_TYPES, VERSION
|
||||
|
||||
class SerialBus:
|
||||
def __init__(self, settings: dict):
|
||||
"""
|
||||
Initialisiert den Bus mit den (ggf. übersteuerten) Settings.
|
||||
"""
|
||||
self.port = settings.get('port')
|
||||
self.baudrate = settings.get('baudrate', 115200)
|
||||
self.timeout = settings.get('timeout', 1.0)
|
||||
self.debug = settings.get('debug', False)
|
||||
self.connection = None
|
||||
|
||||
def open(self):
|
||||
"""Öffnet die serielle Schnittstelle."""
|
||||
try:
|
||||
self.connection = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.flush_input()
|
||||
if self.debug: console.print(f"[bold green]✓[/bold green] Port [info]{self.port}[/info] erfolgreich geöffnet.")
|
||||
from core.cmd.proto import proto
|
||||
cmd = proto(self)
|
||||
data = cmd.get()
|
||||
VERSION["current_protocol_version"] = data['protocol_version'] if data else None
|
||||
if data:
|
||||
if self.debug: console.print(f" • Protokoll Version: [info]{data['protocol_version']}[/info]")
|
||||
if data['protocol_version'] < VERSION["min_protocol_version"] or data['protocol_version'] > VERSION["max_protocol_version"]:
|
||||
if VERSION["min_protocol_version"] == VERSION["max_protocol_version"]:
|
||||
expected = f"Version {VERSION['min_protocol_version']}"
|
||||
else:
|
||||
expected = f"Version {VERSION['min_protocol_version']} bis {VERSION['max_protocol_version']}"
|
||||
raise ValueError(f"Inkompatibles Protokoll. Controller spricht {data['protocol_version']}, erwartet wird {expected}.")
|
||||
else:
|
||||
raise ValueError("Keine gültige Antwort auf Protokollversion erhalten.")
|
||||
except serial.SerialException as e:
|
||||
console_err.print(f"[bold red]Serieller Fehler:[/bold red] [error_msg]{e}[/error_msg]")
|
||||
raise
|
||||
except Exception as e:
|
||||
console_err.print(f"[bold red]Unerwarteter Fehler beim Öffnen:[/bold red] [error_msg]{e}[/error_msg]")
|
||||
raise
|
||||
|
||||
def flush_input(self):
|
||||
"""Leert den Empfangspuffer der seriellen Schnittstelle."""
|
||||
if self.connection and self.connection.is_open:
|
||||
self.connection.reset_input_buffer()
|
||||
|
||||
def close(self):
|
||||
"""Schließt die Verbindung sauber."""
|
||||
if self.connection and self.connection.is_open:
|
||||
self.connection.close()
|
||||
if self.debug: console.print(f"Verbindung zu [info]{self.port}[/info] geschlossen.")
|
||||
|
||||
def send_binary(self, data: bytes):
|
||||
"""Sendet Rohdaten und loggt sie im Hex-Format."""
|
||||
if not self.connection or not self.connection.is_open:
|
||||
raise ConnectionError("Port ist nicht geöffnet.")
|
||||
|
||||
self.connection.write(data)
|
||||
|
||||
if self.debug:
|
||||
hex_data = data.hex(' ').upper()
|
||||
console.print(f"TX -> [grey62]{hex_data}[/grey62]")
|
||||
|
||||
def _read_exact(self, length: int, context: str = "Daten") -> bytes:
|
||||
data = bytearray()
|
||||
while len(data) < length:
|
||||
try:
|
||||
chunk = self.connection.read(length - len(data))
|
||||
except serial.SerialException as e:
|
||||
raise IOError(f"Serielle Verbindung verloren beim Lesen von {context}: {e}") from e
|
||||
if not chunk:
|
||||
raise TimeoutError(f"Timeout beim Lesen von {context}: {len(data)}/{length} Bytes.")
|
||||
data.extend(chunk)
|
||||
return bytes(data)
|
||||
|
||||
def wait_for_sync(self, sync_seq: bytes, max_time: float = 2.0):
|
||||
"""Wartet maximal max_time Sekunden auf die Sync-Sequenz."""
|
||||
buffer = b""
|
||||
start_time = time.time()
|
||||
|
||||
if self.debug:
|
||||
console.print(f"[bold cyan]Warte auf SYNC-Sequenz:[/bold cyan] [grey62]{sync_seq.hex(' ').upper()}[/grey62]")
|
||||
|
||||
# Kurzer interner Timeout für reaktive Schleife
|
||||
original_timeout = self.connection.timeout
|
||||
self.connection.timeout = 0.1
|
||||
|
||||
try:
|
||||
while (time.time() - start_time) < max_time:
|
||||
char = self.connection.read(1)
|
||||
if not char:
|
||||
continue
|
||||
|
||||
buffer += char
|
||||
if len(buffer) > len(sync_seq):
|
||||
buffer = buffer[1:]
|
||||
|
||||
if buffer == sync_seq:
|
||||
if self.debug: console.print("[bold cyan]RX <- SYNC OK[/bold cyan]")
|
||||
return True
|
||||
return False
|
||||
finally:
|
||||
self.connection.timeout = original_timeout
|
||||
|
||||
def send_request(self, cmd_id: int, payload: bytes = b''):
|
||||
self.flush_input()
|
||||
frame_type = struct.pack('B', FRAME_TYPES['request'])
|
||||
cmd_byte = struct.pack('B', cmd_id)
|
||||
|
||||
full_frame = SYNC_SEQ + frame_type + cmd_byte
|
||||
if payload:
|
||||
full_frame += payload
|
||||
self.send_binary(full_frame)
|
||||
|
||||
def receive_ack(self, timeout: float = None):
|
||||
wait_time = timeout if timeout is not None else self.timeout
|
||||
|
||||
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
|
||||
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
|
||||
|
||||
ftype_raw = self.connection.read(1)
|
||||
if not ftype_raw:
|
||||
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
|
||||
ftype = ftype_raw[0]
|
||||
|
||||
if ftype == FRAME_TYPES['error']:
|
||||
err_code_raw = self.connection.read(1)
|
||||
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||
raise ControllerError(err_code, err_name)
|
||||
|
||||
elif ftype == FRAME_TYPES['ack']:
|
||||
if self.debug:
|
||||
console.print(f"[green]ACK empfangen[/green]")
|
||||
return {"type": "ack"}
|
||||
raise ValueError(f"Unerwarteter Frame-Typ (0x{FRAME_TYPES['ack']:02X} (ACK) erwartet): 0x{ftype:02X}")
|
||||
|
||||
def receive_response(self, length: int, timeout: float = None, varlen_params: int = 0):
|
||||
wait_time = timeout if timeout is not None else self.timeout
|
||||
|
||||
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
|
||||
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
|
||||
|
||||
ftype_raw = self.connection.read(1)
|
||||
if not ftype_raw:
|
||||
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
|
||||
ftype = ftype_raw[0]
|
||||
|
||||
if ftype == FRAME_TYPES['error']:
|
||||
err_code_raw = self.connection.read(1)
|
||||
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||
raise ControllerError(err_code, err_name)
|
||||
|
||||
elif ftype == FRAME_TYPES['response']:
|
||||
data = self.connection.read(length)
|
||||
for varlen_param in range(varlen_params):
|
||||
length_byte = self.connection.read(1)
|
||||
if not length_byte:
|
||||
raise TimeoutError("Timeout beim Lesen der Länge eines variablen Parameters.")
|
||||
param_length = length_byte[0]
|
||||
param_data = self.connection.read(param_length)
|
||||
if not param_data:
|
||||
raise TimeoutError("Timeout beim Lesen eines variablen Parameters.")
|
||||
data += length_byte + param_data
|
||||
if self.debug:
|
||||
console.print(f"RX <- [grey62]{data.hex(' ').upper()}[/grey62]")
|
||||
if len(data) < length:
|
||||
raise IOError(f"Unvollständiges Paket: {len(data)}/{length} Bytes.")
|
||||
return {"type": "response", "data": data}
|
||||
|
||||
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
|
||||
|
||||
def receive_list(self):
|
||||
"""Liest eine Liste von Einträgen, bis list_end kommt."""
|
||||
is_list = False
|
||||
list_items = []
|
||||
|
||||
while True:
|
||||
if not self.wait_for_sync(SYNC_SEQ):
|
||||
raise TimeoutError("Timeout beim Warten auf Sync im List-Modus.")
|
||||
|
||||
ftype = self.connection.read(1)[0]
|
||||
|
||||
if ftype == FRAME_TYPES['list_start']:
|
||||
is_list = True
|
||||
list_items = []
|
||||
elif ftype == FRAME_TYPES['list_chunk']:
|
||||
if not is_list: raise ValueError("Chunk ohne Start.")
|
||||
length = struct.unpack('<H', self.connection.read(2))[0]
|
||||
if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||
data = self.connection.read(length)
|
||||
if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||
list_items.append(data)
|
||||
elif ftype == FRAME_TYPES['list_end']:
|
||||
if not is_list: raise ValueError("Ende ohne Start.")
|
||||
num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||
if len(list_items) != num_entries:
|
||||
console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||
return list_items
|
||||
elif ftype == FRAME_TYPES['error']:
|
||||
err_code_raw = self.connection.read(1)
|
||||
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||
raise ControllerError(err_code, err_name)
|
||||
|
||||
def receive_stream(self, chunk_size: int = 1024):
|
||||
"""Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt."""
|
||||
is_stream = False
|
||||
data_chunks = []
|
||||
|
||||
while True:
|
||||
if not self.wait_for_sync(SYNC_SEQ):
|
||||
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
|
||||
|
||||
ftype = self._read_exact(1, "Frame-Typ")[0]
|
||||
if self.debug: console.print(f"Empfangener Frame-Typ: 0x{ftype:02X} (start: 0x{FRAME_TYPES['stream_start']:02X}, chunk: 0x{FRAME_TYPES['stream_chunk']:02X}, end: 0x{FRAME_TYPES['stream_end']:02X})")
|
||||
|
||||
if ftype == FRAME_TYPES['stream_start']:
|
||||
is_stream = True
|
||||
data_chunks = []
|
||||
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
|
||||
if self.debug: console.print(f"Stream gestartet, erwartete Gesamtgröße: {size} Bytes")
|
||||
|
||||
received_size = 0
|
||||
while received_size < size:
|
||||
chunk_length = min(chunk_size, size - received_size)
|
||||
try:
|
||||
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
||||
except Exception as e:
|
||||
raise IOError(f"Stream-Abbruch bei {received_size}/{size} Bytes: {e}") from e
|
||||
data_chunks.append(chunk_data)
|
||||
received_size += len(chunk_data)
|
||||
if self.debug: console.print(f"Empfangen: {received_size}/{size} Bytes ({(received_size/size)*100:.2f}%)")
|
||||
if self.debug: console.print("Stream vollständig empfangen.")
|
||||
|
||||
elif ftype == FRAME_TYPES['stream_end']:
|
||||
if not is_stream: raise ValueError("Ende ohne Start.")
|
||||
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
|
||||
if self.debug: console.print(f"Stream-Ende empfangen, CRC32: 0x{crc32:08X}")
|
||||
|
||||
return {'data': b''.join(data_chunks), 'crc32': crc32}
|
||||
|
||||
# elif ftype == FRAME_TYPES['list_chunk']:
|
||||
# if not is_list: raise ValueError("Chunk ohne Start.")
|
||||
# length = struct.unpack('<H', self.connection.read(2))[0]
|
||||
# if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||
# data = self.connection.read(length)
|
||||
# if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||
# list_items.append(data)
|
||||
# elif ftype == FRAME_TYPES['list_end']:
|
||||
# if not is_list: raise ValueError("Ende ohne Start.")
|
||||
# num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||
# if len(list_items) != num_entries:
|
||||
# console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||
# return list_items
|
||||
elif ftype == FRAME_TYPES['error']:
|
||||
err_code_raw = self.connection.read(1)
|
||||
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||
raise ControllerError(err_code, err_name)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
|
||||
|
||||
class ControllerError(Exception):
|
||||
"""Wird ausgelöst, wenn der Controller einen Error-Frame (0xFF) sendet."""
|
||||
def __init__(self, code, name):
|
||||
self.code = code
|
||||
self.name = name
|
||||
super().__init__(f"Controller Error 0x{code:02X} ({name})")
|
||||
14
tool/core/utils.py
Normal file
14
tool/core/utils.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
custom_theme = Theme({
|
||||
"info": "bold blue",
|
||||
"warning": "yellow",
|
||||
"error": "bold red",
|
||||
"error_msg": "red",
|
||||
"sync": "bold magenta",
|
||||
"wait": "italic grey50"
|
||||
})
|
||||
|
||||
console = Console(theme=custom_theme, highlight=False)
|
||||
console_err = Console(theme=custom_theme, stderr=True, highlight=False)
|
||||
Reference in New Issue
Block a user