Skip to content

Commit d67daa2

Browse files
committed
integrate glavo and fix the canota update
1 parent 6e7e33f commit d67daa2

4 files changed

Lines changed: 182 additions & 105 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ docs/build_*
2020
build/lib/uc2rest/modules.py
2121

2222
conda-out/*
23+
.DS_Store

uc2rest/canota.py

Lines changed: 107 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -440,19 +440,23 @@ def _streaming_upload_worker(self, can_id, port, baud, firmware_data, firmware_s
440440
}
441441
print(" Sending STREAM_START command...", start_cmd)
442442
response = self._send_json(ser, start_cmd)
443-
# {"task": "/can_ota_stream", "canid": 11, "action": "start", "firmware_size": 876784, "page_size": 4096, "chunk_size": 512, "md5": "43ba96b4d18c010201762b840476bf83", "qid": 1}
444-
if response and str(response).find("success")<=0:
445-
print(" ERROR: STREAM_START command failed!")
446-
print(f" Response: {response}")
447-
return False
448-
print(" ✓ Streaming session started")
449-
'''b\'[326584][I][CanOtaStreaming.cpp:427] actFromJsonStreaming(): Target CAN ID: 11\\r\\n[326586][I][CanOtaStreaming.cpp:456] actFromJsonStreaming(): Stream OTA START to CAN ID 11: size=876784, page_size=4096, chunk_size=512\\r\\n[326588][I][CanOtaStreaming.cpp:464] actFromJsonStreaming(): Relaying STREAM_START to slave 0x0B\\r\\n[326590][I][CanOtaStreaming.cpp:653] startStreamToSlave(): Starting stream to slave 0x0B: 876784 bytes, 215 pages\\r\\n[326968][I][CanOtaStreaming.cpp:622] handleSlaveStreamResponse(): Slave STREAM_ACK: page=65535, bytes=0, nextSeq=0\\r\\n++\\n{"success":true,"qid":1}\\n--\\n\\x00\''''
450-
#self._drain_serial(ser)
451-
#self._send_json(ser, {"task": "/can_act", "debug": 1})
452-
443+
444+
# Wait for JSON response
445+
if response and str(response).find("success") <= 0:
446+
if status_callback:
447+
status_callback("STREAM_START command failed", False)
448+
status_callback(f"Response: {response}", False)
449+
return
450+
453451
if status_callback:
454452
status_callback("Streaming session started, uploading...", True)
455453

454+
# CRITICAL: Wait for slave to initialize via CAN
455+
time.sleep(1.0)
456+
457+
# Drain any pending data (logs, old ACKs, etc.)
458+
self._drain_serial(ser, "post-start", verbose=False)
459+
456460
# Step 4: Stream pages
457461
start_time = time.time()
458462
seq = 0
@@ -468,14 +472,24 @@ def _streaming_upload_worker(self, can_id, port, baud, firmware_data, firmware_s
468472
if len(page_data) < PAGE_SIZE:
469473
page_data = page_data + bytes(PAGE_SIZE - len(page_data))
470474

475+
# Progress display
476+
progress = (page_idx + 1) / num_pages * 100
477+
elapsed = time.time() - start_time
478+
speed = bytes_sent / elapsed / 1024 if elapsed > 0 else 0
479+
print(f"\r Page {page_idx+1}/{num_pages} ({progress:.1f}%) - {speed:.1f} KB/s ", end="")
480+
481+
471482
# Send page with retry
472-
success, seq = self._send_page_with_retry(ser=ser, page_idx=page_idx, page_data=page_data, seq_start=seq)
483+
# Send page with retry logic
484+
success, seq, acked_page, acked_bytes = self._send_page_with_retry(
485+
ser, page_idx, page_data, seq
486+
)
473487
bytes_sent += PAGE_SIZE
474488

475489
if not success:
476490
if status_callback:
477491
status_callback(f"Page {page_idx} failed after retries", False)
478-
return False
492+
return
479493

480494
# Progress callback
481495
if progress_callback:
@@ -560,7 +574,7 @@ def _drain_serial(self, ser, label="", verbose=True):
560574
print(f" [{label}] Drained {len(data)} bytes")
561575
return data
562576

563-
def _send_json(self, ser, data, wait_response=True, timeout=2.0):
577+
def _send_json(self, ser, data, wait_response=True, timeout=3.0):
564578
"""Send JSON command over serial."""
565579
json_str = json.dumps(data, separators=(',', ':'))
566580
tx_data = (json_str + '\n').encode()
@@ -585,19 +599,22 @@ def _send_json(self, ser, data, wait_response=True, timeout=2.0):
585599
return None
586600

587601
def _build_stream_data_packet(self, page_idx, offset, seq, chunk_data):
588-
"""Build a binary stream data packet."""
589-
header = struct.pack('<HHHH', page_idx, offset, len(chunk_data), seq)
590-
packet_body = bytes([STREAM_DATA]) + header + chunk_data
591-
checksum = sum(packet_body) & 0xFF
592-
return bytes([SYNC_1, SYNC_2]) + packet_body + bytes([checksum])
593-
602+
# Match can_ota_streaming.py wire format + checksum
603+
packet_body = bytes([SYNC_1, SYNC_2, STREAM_DATA]) + struct.pack(
604+
'>HHHH', page_idx, offset, len(chunk_data), seq
605+
) + chunk_data
606+
checksum = 0
607+
for b in packet_body:
608+
checksum ^= b
609+
return packet_body + bytes([checksum])
610+
594611
def _build_stream_finish_packet(self, md5_bytes):
595-
"""Build STREAM_FINISH packet with MD5 hash."""
596-
header = bytes([STREAM_FINISH])
597-
packet_body = header + md5_bytes
598-
checksum = sum(packet_body) & 0xFF
599-
return bytes([SYNC_1, SYNC_2]) + packet_body + bytes([checksum])
600-
612+
packet_body = bytes([SYNC_1, SYNC_2, STREAM_FINISH]) + md5_bytes
613+
checksum = 0
614+
for b in packet_body:
615+
checksum ^= b
616+
return packet_body + bytes([checksum])
617+
601618
def _wait_for_session_start(self, ser, timeout=10.0):
602619
"""Wait for streaming session start ACK."""
603620
start = time.time()
@@ -712,13 +729,73 @@ def _send_page_with_retry(self, ser, page_idx, page_data, seq_start,
712729
ser.flush()
713730

714731
# Wait for ACK
715-
success, acked_page, acked_bytes, raw = self._wait_for_page_ack(ser, page_idx)
732+
success, acked_page, acked_bytes, raw = self.wait_for_stream_ack(ser, page_idx)
716733

717734
if success:
718-
return (True, seq)
735+
return (True, seq, acked_page, acked_bytes)
719736

720737
if retry < max_retries - 1:
721-
time.sleep(0.5)
722-
self._drain_serial(ser)
738+
print(f" [RETRY {retry+1}/{max_retries}]", end="")
739+
time.sleep(0.5) # Small delay before retry
740+
self._drain_serial(ser, "retry", verbose=False)
741+
742+
return (False, seq, -1, 0)
743+
744+
745+
746+
def wait_for_stream_ack(self, ser, expected_page: int, timeout: float = PAGE_ACK_TIMEOUT):
747+
"""
748+
Wait for STREAM_ACK response.
749+
Returns: (success, last_complete_page, bytes_received, raw_response)
750+
"""
751+
start = time.time()
752+
buffer = bytearray()
753+
754+
while time.time() - start < timeout:
755+
if ser.in_waiting:
756+
new_data = ser.read(ser.in_waiting)
757+
buffer.extend(new_data)
758+
759+
# Check for log messages indicating success (for final ACK)
760+
try:
761+
text = bytes(buffer).decode('utf-8', errors='replace')
762+
print("DEBUG TEXT:", text)
763+
if "OTA COMPLETE" in text or "Rebooting" in text:
764+
return (True, expected_page, 0, bytes(buffer))
765+
except:
766+
pass
767+
768+
# Search for STREAM_ACK response
769+
# Format: [SYNC][CMD][status][canId][lastPage_L][lastPage_H][bytes(4)][nextSeq(2)][reserved(2)]
770+
i = 0
771+
while i < len(buffer) - 15:
772+
if buffer[i] == SYNC_1 and buffer[i+1] == SYNC_2:
773+
cmd = buffer[i+2]
774+
775+
if cmd == STREAM_ACK:
776+
status = buffer[i+3]
777+
can_id = buffer[i+4]
778+
last_page = buffer[i+5] | (buffer[i+6] << 8) # Little endian
779+
bytes_recv = struct.unpack('<I', bytes(buffer[i+7:i+11]))[0]
780+
next_seq = buffer[i+11] | (buffer[i+12] << 8)
781+
782+
if status == 0: # CAN_OTA_OK
783+
return (True, last_page, bytes_recv, bytes(buffer))
784+
else:
785+
print(f" STREAM_ACK with error status: {status}")
786+
return (False, last_page, bytes_recv, bytes(buffer))
787+
788+
elif cmd == STREAM_NAK:
789+
status = buffer[i+3]
790+
can_id = buffer[i+4]
791+
error_page = buffer[i+5] | (buffer[i+6] << 8)
792+
missing_offset = buffer[i+7] | (buffer[i+8] << 8)
793+
794+
print(f" STREAM_NAK: status={status}, page={error_page}, offset={missing_offset}")
795+
return (False, error_page, missing_offset, bytes(buffer))
796+
797+
i += 1
798+
else:
799+
time.sleep(0.001)
723800

724-
return (False, seq)
801+
return (False, -1, 0, bytes(buffer))

uc2rest/galvo.py

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -37,80 +37,81 @@ def set_dac(self, channel=1, frequency=1, offset=0, amplitude=1, clk_div=0, phas
3737
SCANNER
3838
##############################################################################################################################
3939
'''
40-
def set_scanner_pattern(self, numpyPattern, scannernFrames=1,
41-
scannerLaserVal=32000,
42-
scannerExposure=500, scannerDelay=500, is_blocking = False):
43-
44-
scannerMode="pattern"
45-
path = '/scanner_act'
46-
arraySize = int(np.prod(numpyPattern.shape))
40+
def set_galvo_scan(self, nx=256, ny=256, x_min=500, x_max=3500,
41+
y_min=500, y_max=3500, sample_period_us=1,
42+
frame_count=0, bidirectional=False, timeout=1):
43+
"""
44+
Start galvo scanner with new API (HighSpeedScannerCore)
45+
46+
Args:
47+
nx: Number of X samples per line (default: 256)
48+
ny: Number of Y lines (default: 256)
49+
x_min: Min X position 0-4095 (default: 500)
50+
x_max: Max X position 0-4095 (default: 3500)
51+
y_min: Min Y position 0-4095 (default: 500)
52+
y_max: Max Y position 0-4095 (default: 3500)
53+
sample_period_us: Microseconds per sample, 0=max speed (default: 1)
54+
frame_count: Number of frames, 0=infinite (default: 0)
55+
bidirectional: Enable bidirectional scanning (default: False)
56+
timeout: Request timeout in seconds (default: 1)
57+
58+
Example:
59+
>>> galvo.set_galvo_scan(nx=64, ny=64, frame_count=10, bidirectional=True)
60+
"""
61+
path = '/galvo_act'
4762
payload = {
48-
"task":path,
49-
"scannernFrames":scannernFrames,
50-
"scannerMode":scannerMode,
51-
"arraySize":arraySize,
52-
"i":numpyPattern.flatten().tolist(),
53-
"scannerLaserVal":scannerLaserVal,
54-
"scannerExposure":scannerExposure,
55-
"scannerDelay":scannerDelay,
56-
"isblock": is_blocking
63+
"task": path,
64+
"config": {
65+
"nx": nx,
66+
"ny": ny,
67+
"x_min": x_min,
68+
"x_max": x_max,
69+
"y_min": y_min,
70+
"y_max": y_max,
71+
"sample_period_us": sample_period_us,
72+
"frame_count": frame_count,
73+
"bidirectional": 1 if bidirectional else 0
5774
}
58-
59-
r = self.post_json(path, payload)
60-
return r
61-
62-
def set_scanner_classic(self, scannernFrames=100,
63-
scannerXFrameMin=0, scannerXFrameMax=255,
64-
scannerYFrameMin=0, scannerYFrameMax=255,
65-
scannerEnable=0, scannerxMin=1,
66-
scannerxMax=5, scanneryMin=1,
67-
scanneryMax=5, scannerXStep=25,
68-
scannerYStep=25, scannerLaserVal=32000,
69-
scannerExposure=500, scannerDelay=500):
70-
71-
scannerModec="classic",
72-
path = '/scanner_act'
75+
}
76+
77+
return self._parent.post_json(path, payload, timeout=timeout)
78+
79+
def stop_galvo_scan(self, timeout=1):
80+
"""
81+
Stop galvo scanner
82+
83+
Args:
84+
timeout: Request timeout in seconds (default: 1)
85+
86+
Example:
87+
>>> galvo.stop_galvo_scan()
88+
"""
89+
path = '/galvo_act'
7390
payload = {
74-
"task":path,
75-
"scannernFrames":scannernFrames,
76-
"scannerMode":scannerModec,
77-
"scannerXFrameMin":scannerXFrameMin,
78-
"scannerXFrameMax":scannerXFrameMax,
79-
"scannerYFrameMin":scannerYFrameMin,
80-
"scannerYFrameMax":scannerYFrameMax,
81-
"scannerEnable":scannerEnable,
82-
"scannerxMin":scannerxMin,
83-
"scannerxMax":scannerxMax,
84-
"scanneryMin":scanneryMin,
85-
"scanneryMax":scanneryMax,
86-
"scannerXStep":scannerXStep,
87-
"scannerYStep":scannerYStep,
88-
"scannerLaserVal":scannerLaserVal,
89-
"scannerExposure":scannerExposure,
90-
"scannerDelay":scannerDelay}
91-
92-
r = self.post_json(path, payload)
93-
return r
94-
95-
96-
def set_galvo_freq(self, axis=1, value=1000):
97-
if axis+1 == 1:
98-
self.galvo1.frequency=value
99-
payload = self.galvo1.return_dict()
100-
else:
101-
self.galvo2.frequency=value
102-
payload = self.galvo2.return_dict()
103-
104-
r = self.post_json(payload["task"], payload, timeout=1)
105-
return r
106-
107-
def set_galvo_amp(self, axis=1, value=1000):
108-
if axis+1 == 1:
109-
self.galvo1.amplitude=value
110-
payload = self.galvo1.return_dict()
111-
else:
112-
self.galvo2.amplitude=value
113-
payload = self.galvo2.return_dict()
91+
"task": path,
92+
"stop": True
93+
}
94+
95+
return self._parent.post_json(path, payload, timeout=timeout)
96+
97+
def get_galvo_status(self, timeout=1):
98+
"""
99+
Get galvo scanner status
100+
101+
Args:
102+
timeout: Request timeout in seconds (default: 1)
103+
104+
Returns:
105+
dict: Status including running, current_frame, current_line, config, etc.
106+
107+
Example:
108+
>>> status = galvo.get_galvo_status()
109+
>>> print(f"Running: {status['running']}, Frame: {status['current_frame']}")
110+
"""
111+
path = '/galvo_get'
112+
payload = {
113+
"task": path
114+
}
115+
116+
return self._parent.post_json(path, payload, timeout=timeout)
114117

115-
r = self.post_json(payload["task"], payload, timeout=1)
116-
return r

uc2rest/motor.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,8 +1031,6 @@ def stop_stage_scanning(self):
10311031
r = self._parent.post_json(path, payload)
10321032
return r
10331033

1034-
1035-
10361034
def start_stage_scanning(self, xstart=0, xstep=1000, nx=20, ystart=0, ystep=1000, ny=10, zstart=0, zstep=1000, nz=10, tsettle=5, tExposure=50, illumination=(0,0,0,0), led=0, speed=20000, acceleration=None):
10371035
# {"task": "/motor_act", "stagescan": {"xStart": 0, "yStart": 0, "zStart": 0, "xStep": 500, "yStep": 500, "zStep": 500, "nX": 10, "nY": 10, "nZ": 10, "tPre": 50, "tPost": 50, "illumination": [0, 1, 0, 0], "led": 255}}
10381036
if acceleration is None:

0 commit comments

Comments
 (0)