Initial commit
This commit is contained in:
		
						commit
						735e5de328
					
				
					 25 changed files with 894 additions and 0 deletions
				
			
		
							
								
								
									
										0
									
								
								core/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								core/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								core/advertisement.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								core/advertisement.py
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										86
									
								
								core/protocol.py
									
										
									
									
									
										Normal 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)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue