Initial commit

This commit is contained in:
MassiveBox 2025-11-01 00:16:33 +01:00
commit 735e5de328
Signed by: massivebox
GPG key ID: 9B74D3A59181947D
25 changed files with 894 additions and 0 deletions

0
core/__init__.py Normal file
View file

33
core/advertisement.py Normal file
View file

@ -0,0 +1,33 @@
import dataclasses
from ble.interface import Advertisement
from image.conversion import DEVICE_SPECS, DeviceSpec, ModelId
@dataclasses.dataclass
class DeviceData:
name: str
model: ModelId
firmware: int
hardware: int
battery: float
voltage: float
def parse_advertisement(adv: Advertisement) -> DeviceData:
data = adv.manufacturer_data[0x5053]
device_id = data[0]
volt = data[1] / 10
firmware = (data[2] << 8) + data[3]
hardware = (data[0] << 8) + data[4]
device = DEVICE_SPECS[device_id]
return DeviceData(
name=adv.name,
model=device_id,
firmware=firmware,
hardware=hardware,
battery=(volt - device.min_voltage) * 100 / (device.max_voltage - device.min_voltage),
voltage=volt
)

86
core/protocol.py Normal file
View file

@ -0,0 +1,86 @@
import logging
import sys, asyncio, struct, time
from typing import Optional
from ble.interface import BLEInterface, Advertisement
from logger.logger import GiciskyLogger, LogCategory
# Default UUIDs for Gicisky tags
BASE_SERVICE = 0xFEF0
SERVICE_UUID = f"0000{BASE_SERVICE:04x}-0000-1000-8000-00805f9b34fb"
CHAR_CMD_UUID = f"0000{BASE_SERVICE+1:04x}-0000-1000-8000-00805f9b34fb"
CHAR_IMG_UUID = f"0000{BASE_SERVICE+2:04x}-0000-1000-8000-00805f9b34fb"
# Constants
CHUNK_SIZE = 240 # 480 hex chars (as seen in the JS uploader)
MTU_WAIT = 0.03 # delay between chunks (adjustable)
DEBUG = True
def log(msg):
if DEBUG:
print(f"[LOG] {msg}", flush=True)
class GiciskyProtocol:
def __init__(self, ble: BLEInterface, logger: Optional[logging.Logger] = None):
self.ble = ble
self.packet_index = 0
self.ready_to_send = False
self.img_hex = b""
self.upload_done = False
self.logger = GiciskyLogger(logger)
async def handle_notification(self, data):
hx = data.hex()
self.logger.debug(f"Notify: {hx}", LogCategory.NOTIFICATION)
if hx.startswith("01f400"):
# Step 2: ready to accept image size
self.logger.info("Device ready to accept image size", LogCategory.PROTOCOL)
await self.send_command(2, struct.pack("<I", int(len(self.img_hex))) + b"\x00\x00\x00")
elif hx.startswith("02"):
# Step 3: begin upload
self.logger.info("Starting image upload", LogCategory.DATA_TRANSFER)
await self.send_command(3)
elif hx.startswith("05"):
self.logger.debug(f"Handling response: {hx}, err: {hx[2:4]}, part: {hx[4:12]}", LogCategory.NOTIFICATION)
err = hx[2:4]
if err == "00": # continue sending chunks
await self.send_next_chunk(hx[4:12])
elif err == "08": # upload complete
self.logger.info("Upload completed successfully.", LogCategory.DATA_TRANSFER)
self.img_hex = b""
self.upload_done = True
else:
self.logger.error(f"Error code during upload: {err}", LogCategory.DATA_TRANSFER)
async def send_command(self, cmd_id, payload=b""):
pkt = bytes([cmd_id]) + payload
await self.ble.write(CHAR_CMD_UUID, pkt)
self.logger.debug(f"Cmd {cmd_id:02x} sent: {pkt.hex()}", LogCategory.COMMAND)
async def send_next_chunk(self, ack_hex):
ack = struct.unpack("<I", bytes.fromhex(ack_hex))[0]
if not self.img_hex:
self.logger.debug("No more data to send.", LogCategory.DATA_TRANSFER)
return
if ack == self.packet_index:
prefix = struct.pack("<I", self.packet_index)
self.packet_index += 1
chunk = self.img_hex[:CHUNK_SIZE]
self.img_hex = self.img_hex[CHUNK_SIZE:]
await self.ble.write(CHAR_IMG_UUID, prefix + chunk)
self.logger.debug(f"Sent packet #{self.packet_index - 1} {ack_hex}", LogCategory.DATA_TRANSFER)
await asyncio.sleep(MTU_WAIT)
else:
self.logger.warning(f"ACK mismatch ({ack} != {self.packet_index})", LogCategory.DATA_TRANSFER)
async def upload_image(self, img: bytes):
self.img_hex = img
await self.ble.start_notify(CHAR_CMD_UUID, self.handle_notification)
self.logger.info("Notifications started.", LogCategory.PROTOCOL)
await self.send_command(1) # init command
while not self.upload_done:
await asyncio.sleep(0.5)