diff --git a/__pycache__/device_commands.cpython-312.pyc b/__pycache__/device_commands.cpython-312.pyc index 05daede..847740a 100644 Binary files a/__pycache__/device_commands.cpython-312.pyc and b/__pycache__/device_commands.cpython-312.pyc differ diff --git a/__pycache__/device_interaction.cpython-312.pyc b/__pycache__/device_interaction.cpython-312.pyc index 07fbdd1..5179eca 100644 Binary files a/__pycache__/device_interaction.cpython-312.pyc and b/__pycache__/device_interaction.cpython-312.pyc differ diff --git a/__pycache__/gui.cpython-312.pyc b/__pycache__/gui.cpython-312.pyc index ddf0b4c..b7d5e24 100644 Binary files a/__pycache__/gui.cpython-312.pyc and b/__pycache__/gui.cpython-312.pyc differ diff --git a/_device_main.py b/_device_main.py index 92f600e..88c5892 100644 --- a/_device_main.py +++ b/_device_main.py @@ -1,6 +1,7 @@ from FreeSimpleGUI import TIMEOUT_KEY, WIN_CLOSED import json import math +import os import socket import subprocess import time @@ -34,6 +35,8 @@ DS1809_INIT_STARTUP_DELAY_S = 0.35 STM32_DAC_VREF = 2.5 STM32_DAC_MAX_CODE = 4095 PA4_DAC_DEFAULT_VOLT = 0.52 +AD9102_WAVEFORM_FILE_NAME = "waveform.json" +AD9102_WAVEFORM_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), AD9102_WAVEFORM_FILE_NAME) #### ---- Functions @@ -162,6 +165,7 @@ def set_initial_params(): params['RampSramMode'] = False params['RampSramSamples'] = '' params['RampSramAmp'] = '' + params['WaveformStatus'] = f'{AD9102_WAVEFORM_FILE_NAME}: не загружен' params['Ad9833Freq'] = str(AD9833_FREQ_DEFAULT_KHZ) params['Ad9833Mclk'] = str(AD9833_MCLK_DEFAULT_MHZ) params['Ad9833Triangle'] = True @@ -573,6 +577,15 @@ if __name__ == "__main__": mclk_hz = mclk_mhz * 1e6 if mclk_mhz is not None else None triangle = values.get('-AD9833Triangle-', True) dev.start_ad9833_ramp(prt, freq_hz=freq_hz, mclk_hz=mclk_hz, triangle=triangle, enable=True) + elif event == '-UploadWaveform-': + try: + waveform = dev.load_ad9102_waveform_file(AD9102_WAVEFORM_PATH) + dev.upload_ad9102_waveform(prt, waveform) + params['WaveformStatus'] = f'{AD9102_WAVEFORM_FILE_NAME}: {len(waveform)} точек загружено' + except Exception as exc: + print(f'AD9102 waveform upload failed: {exc}') + params['WaveformStatus'] = f'{AD9102_WAVEFORM_FILE_NAME}: ошибка - {exc}' + window['-WaveformStatus-'].update(params['WaveformStatus']) elif event == '-DS1809UC-': dev.send_ds1809_pulse(prt, uc=True, dc=False, count=1, pulse_ms=DS1809_INIT_PULSE_MS) ds1809_step = clamp_int(ds1809_step + 1, 0, DS1809_MAX_STEP) diff --git a/device_commands.py b/device_commands.py index db9a6d3..96064ed 100644 --- a/device_commands.py +++ b/device_commands.py @@ -18,6 +18,10 @@ DS1809_CMD_TOTAL_LENGTH = 10 # Total bytes when sending DS1809 UC/DC pulse comma DS1809_CMD_HEADER = "AAAA" STM32_DAC_CMD_TOTAL_LENGTH = 10 # Total bytes when sending STM32 DAC command STM32_DAC_CMD_HEADER = "BBBB" +AD9102_WAVE_CTRL_TOTAL_LENGTH = 10 # Total bytes when sending AD9102 waveform control command +AD9102_WAVE_CTRL_HEADER = "CCCC" +AD9102_WAVE_DATA_TOTAL_LENGTH = 30 # Total bytes when sending AD9102 waveform data packet +AD9102_WAVE_DATA_HEADER = "DDDD" AD9102_SAW_STEP_DEFAULT = 1 AD9102_PAT_PERIOD_DEFAULT = 0xFFFF AD9102_PAT_PERIOD_BASE_DEFAULT = 0x02 @@ -27,6 +31,13 @@ AD9102_FLAG_SRAM_FMT = 0x0008 AD9102_SRAM_SAMPLES_DEFAULT = 16 AD9102_SRAM_HOLD_DEFAULT = 1 AD9102_SRAM_AMP_DEFAULT = 8191 +AD9102_WAVE_OPCODE_BEGIN = 0x0001 +AD9102_WAVE_OPCODE_COMMIT = 0x0002 +AD9102_WAVE_OPCODE_CANCEL = 0x0003 +AD9102_WAVE_CHUNK_SAMPLES = 12 +AD9102_SAMPLE_MIN = -8192 +AD9102_SAMPLE_MAX = 8191 +AD9102_WAVE_MAX_SAMPLES = 4096 AD9833_FLAG_ENABLE = 0x0001 AD9833_FLAG_TRIANGLE = 0x0002 AD9833_MCLK_HZ_DEFAULT = 20_000_000 @@ -219,17 +230,45 @@ def send_STM32_DAC(prt, bytestring): print("Sent: STM32 DAC command.") +def send_AD9102_waveform_control(prt, bytestring, quiet: bool = False): + ''' Control custom AD9102 SRAM upload (0xCCCC + ...). + Expected device answer: STATE. + ''' + if len(bytestring) != AD9102_WAVE_CTRL_TOTAL_LENGTH: + print("Error. Wrong parameter string for AD9102 waveform control command.") + return None + prt.write(bytestring) + if not quiet: + print("Sent: AD9102 waveform control command.") + + +def send_AD9102_waveform_data(prt, bytestring, quiet: bool = False): + ''' Send custom AD9102 SRAM samples (0xDDDD + ...). + Expected device answer: STATE. + ''' + if len(bytestring) != AD9102_WAVE_DATA_TOTAL_LENGTH: + print("Error. Wrong parameter string for AD9102 waveform data command.") + return None + prt.write(bytestring) + if not quiet: + print("Sent: AD9102 waveform data command.") + + # ---- Getting data -def get_STATE(prt): +def get_STATE(prt, quiet: bool = False): ''' Get decoded state of the device in byte format (2 bytes). ''' - print("Received "+str(prt.inWaiting())+" bytes.") + if not quiet: + print("Received "+str(prt.inWaiting())+" bytes.") if prt.inWaiting()!=2: - print("Error. Couldn't get STATE data. prt.inWaiting():", prt.inWaiting()) - print("Flushing input data:", prt.read(prt.inWaiting())) + if not quiet: + print("Error. Couldn't get STATE data. prt.inWaiting():", prt.inWaiting()) + print("Flushing input data:", prt.read(prt.inWaiting())) + else: + prt.read(prt.inWaiting()) # print("Flushing input data:", prt.read(2), prt.read(2)) return None @@ -534,6 +573,61 @@ def create_STM32_DAC_command(dac_code: int, enable: bool = True): return bytearray.fromhex(data) +def create_AD9102_waveform_control_command(opcode: int, param0: int = 0, param1: int = 0): + if opcode is None: + opcode = 0 + if param0 is None: + param0 = 0 + if param1 is None: + param1 = 0 + + opcode &= 0xFFFF + param0 &= 0xFFFF + param1 &= 0xFFFF + crc_word = opcode ^ param0 ^ param1 + + data = flipfour(AD9102_WAVE_CTRL_HEADER) # Word 0 (header) + data += flipfour(int_to_hex(opcode)) + data += flipfour(int_to_hex(param0)) + data += flipfour(int_to_hex(param1)) + data += flipfour(int_to_hex(crc_word)) + + return bytearray.fromhex(data) + + +def create_AD9102_waveform_data_command(samples_chunk): + if samples_chunk is None: + raise ValueError("AD9102 waveform chunk is missing.") + + samples = list(samples_chunk) + chunk_count = len(samples) + if chunk_count < 1 or chunk_count > AD9102_WAVE_CHUNK_SAMPLES: + raise ValueError("AD9102 waveform chunk size must be within [1, 12].") + + encoded_words = [chunk_count] + for sample in samples: + if isinstance(sample, bool) or not isinstance(sample, int): + raise ValueError("AD9102 waveform samples must be integers.") + if sample < AD9102_SAMPLE_MIN or sample > AD9102_SAMPLE_MAX: + raise ValueError("AD9102 waveform sample is out of range [-8192, 8191].") + encoded_words.append(sample & 0xFFFF) + + while len(encoded_words) < (1 + AD9102_WAVE_CHUNK_SAMPLES): + encoded_words.append(0) + + crc_word = 0 + for word in encoded_words: + crc_word ^= word + + data = flipfour(AD9102_WAVE_DATA_HEADER) # Word 0 (header) + data += flipfour(int_to_hex(encoded_words[0])) + for word in encoded_words[1:]: + data += flipfour(int_to_hex(word)) + data += flipfour(int_to_hex(crc_word)) + + return bytearray.fromhex(data) + + def encode_Input(params): if params is None: diff --git a/device_interaction.py b/device_interaction.py index ba1de18..ad2280e 100644 --- a/device_interaction.py +++ b/device_interaction.py @@ -1,4 +1,5 @@ import time +import json from datetime import datetime import device_commands as cmd @@ -7,19 +8,54 @@ import device_commands as cmd #### ---- Constants WAIT_AFTER_SEND = 0.15 # Wait after sending command before requesting input (s). +FAST_STATE_TIMEOUT_S = 0.4 +FAST_STATE_POLL_S = 0.005 #### ---- High-level port commands +def _port_sort_key(port_info): + device = str(getattr(port_info, "device", "") or "").lower() + description = str(getattr(port_info, "description", "") or "").lower() + hwid = str(getattr(port_info, "hwid", "") or "").lower() + + is_usb = ("usb" in device) or ("acm" in device) or ("usb" in description) or ("vid:pid" in hwid) + is_builtin_uart = device.startswith("/dev/ttys") + + return ( + 0 if is_usb else 1, + 1 if is_builtin_uart else 0, + device, + ) + + +def _is_preferred_serial_port(port_info): + device = str(getattr(port_info, "device", "") or "").lower() + description = str(getattr(port_info, "description", "") or "").lower() + hwid = str(getattr(port_info, "hwid", "") or "").lower() + return ("usb" in device) or ("acm" in device) or ("usb" in description) or ("vid:pid" in hwid) + + def create_port_connection(): prt = None - for port, _, _ in sorted(cmd.list_ports.comports()): + port_infos = list(cmd.list_ports.comports()) + preferred_ports = [port_info for port_info in port_infos if _is_preferred_serial_port(port_info)] + if preferred_ports: + port_infos = preferred_ports + + for port_info in sorted(port_infos, key=_port_sort_key): + port = getattr(port_info, "device", None) + if not port: + continue try: prt = cmd.setup_port_connection(port=port, baudrate=115200, timeout_sec=1) cmd.open_port(prt) - reset_port_settings(prt) - except: - prt.close() + if not reset_port_settings(prt): + raise RuntimeError(f"No valid STATE reply on {port}") + except Exception: + if prt is not None and prt.is_open: + prt.close() + prt = None continue break return prt @@ -35,14 +71,71 @@ def _print_state_reply(state_bytes): return True +def _decode_state_word(state_bytes): + if state_bytes is None: + return None + try: + return int(cmd.flipfour(state_bytes.hex()), 16) + except Exception: + return None + + +def _state_has_errors(state_bytes): + state_word = _decode_state_word(state_bytes) + if state_word is None: + return True + return (state_word & 0x00FF) != 0 + + +def _state_message(state_bytes): + if state_bytes is None: + return "STATE reply not received." + return cmd.decode_STATE(state_bytes.hex()) + + +def _wait_for_state_reply(prt, timeout_s=WAIT_AFTER_SEND, poll_s=0.01, quiet=False): + if not _wait_for_min_bytes(prt, expected_len=2, timeout_s=timeout_s, poll_s=poll_s): + if not quiet: + print("Error. Timed out waiting for STATE.") + return None + + state_bytes = cmd.get_STATE(prt, quiet=quiet) + if quiet: + return state_bytes + _print_state_reply(state_bytes) + return state_bytes + + +def _rollback_ad9102_waveform_upload(prt): + try: + cancel_cmd = cmd.create_AD9102_waveform_control_command(cmd.AD9102_WAVE_OPCODE_CANCEL) + cmd.send_AD9102_waveform_control(prt, cancel_cmd, quiet=True) + state_bytes = _wait_for_state_reply(prt, timeout_s=FAST_STATE_TIMEOUT_S, + poll_s=FAST_STATE_POLL_S, quiet=True) + if state_bytes is not None and not _state_has_errors(state_bytes): + return + except Exception: + pass + + reset_port_settings(prt) + + def reset_port_settings(prt): # Reset port settings and check status + try: + prt.reset_input_buffer() + prt.reset_output_buffer() + except Exception: + pass + cmd.send_DEFAULT_ENABLE(prt) - time.sleep(WAIT_AFTER_SEND) - status = cmd.get_STATE(prt).hex() - if status is not None: + state_bytes = _wait_for_state_reply(prt, timeout_s=max(WAIT_AFTER_SEND, 0.4), poll_s=0.01, quiet=True) + if state_bytes is not None: + status = state_bytes.hex() print("Received: STATE. State status:", cmd.decode_STATE(status), "(" + cmd.flipfour(status) + ")") print("") + return True + return False def request_state(prt): @@ -143,6 +236,76 @@ def set_stm32_dac(prt, dac_code, enable=True): return _print_state_reply(cmd.get_STATE(prt)) +def load_ad9102_waveform_file(filepath): + try: + with open(filepath, "r", encoding="utf-8") as fh: + payload = json.load(fh) + except FileNotFoundError as exc: + raise ValueError(f"Waveform file not found: {filepath}") from exc + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON in waveform file: {exc.msg}") from exc + + if not isinstance(payload, list): + raise ValueError("Waveform file must contain a JSON array.") + + if len(payload) < 2 or len(payload) > cmd.AD9102_WAVE_MAX_SAMPLES: + raise ValueError(f"Waveform length must be within [2, {cmd.AD9102_WAVE_MAX_SAMPLES}].") + + samples = [] + for index, sample in enumerate(payload): + if isinstance(sample, bool) or not isinstance(sample, int): + raise ValueError(f"Waveform sample #{index} is not an integer.") + if sample < cmd.AD9102_SAMPLE_MIN or sample > cmd.AD9102_SAMPLE_MAX: + raise ValueError( + f"Waveform sample #{index} is out of range " + f"[{cmd.AD9102_SAMPLE_MIN}, {cmd.AD9102_SAMPLE_MAX}]." + ) + samples.append(int(sample)) + + return samples + + +def upload_ad9102_waveform(prt, samples): + waveform = list(samples) if samples is not None else [] + if len(waveform) < 2 or len(waveform) > cmd.AD9102_WAVE_MAX_SAMPLES: + raise ValueError(f"Waveform length must be within [2, {cmd.AD9102_WAVE_MAX_SAMPLES}].") + + try: + begin_cmd = cmd.create_AD9102_waveform_control_command( + cmd.AD9102_WAVE_OPCODE_BEGIN, + param0=len(waveform), + param1=0, + ) + cmd.send_AD9102_waveform_control(prt, begin_cmd, quiet=True) + state_bytes = _wait_for_state_reply(prt, timeout_s=FAST_STATE_TIMEOUT_S, + poll_s=FAST_STATE_POLL_S, quiet=True) + if state_bytes is None or _state_has_errors(state_bytes): + raise RuntimeError(f"Waveform BEGIN failed: {_state_message(state_bytes)}") + + for offset in range(0, len(waveform), cmd.AD9102_WAVE_CHUNK_SAMPLES): + chunk = waveform[offset:offset + cmd.AD9102_WAVE_CHUNK_SAMPLES] + chunk_cmd = cmd.create_AD9102_waveform_data_command(chunk) + cmd.send_AD9102_waveform_data(prt, chunk_cmd, quiet=True) + state_bytes = _wait_for_state_reply(prt, timeout_s=FAST_STATE_TIMEOUT_S, + poll_s=FAST_STATE_POLL_S, quiet=True) + if state_bytes is None or _state_has_errors(state_bytes): + chunk_no = (offset // cmd.AD9102_WAVE_CHUNK_SAMPLES) + 1 + raise RuntimeError(f"Waveform DATA chunk {chunk_no} failed: {_state_message(state_bytes)}") + + commit_cmd = cmd.create_AD9102_waveform_control_command(cmd.AD9102_WAVE_OPCODE_COMMIT) + cmd.send_AD9102_waveform_control(prt, commit_cmd, quiet=True) + state_bytes = _wait_for_state_reply(prt, timeout_s=FAST_STATE_TIMEOUT_S, + poll_s=FAST_STATE_POLL_S, quiet=True) + if state_bytes is None or _state_has_errors(state_bytes): + raise RuntimeError(f"Waveform COMMIT failed: {_state_message(state_bytes)}") + + print(f"Uploaded AD9102 waveform ({len(waveform)} samples).") + return True + except Exception: + _rollback_ad9102_waveform_upload(prt) + raise + + def _wait_for_min_bytes(prt, expected_len, timeout_s, poll_s=0.01): deadline = time.time() + timeout_s while time.time() < deadline: diff --git a/gui.py b/gui.py index 05484b5..9ef4fae 100644 --- a/gui.py +++ b/gui.py @@ -50,6 +50,8 @@ SET_RAMP_TRI_TEXT = 'Треугольник' SET_RAMP_SRAM_MODE_TEXT = 'SRAM режим' SET_RAMP_SRAM_SAMPLES_TEXT = 'SRAM точки (samples):' SET_RAMP_SRAM_AMP_TEXT = 'SRAM амплитуда (%):' +SET_WAVEFORM_BUTTON_TEXT = 'Загрузить форму' +SET_WAVEFORM_STATUS_TEXT = 'Форма AD9102:' SET_AD9833_SECTION_TEXT = 'Настройка пилы (AD9833)' SET_AD9833_FREQ_TEXT = 'Частота AD9833 (кГц):' SET_AD9833_MCLK_TEXT = 'MCLK AD9833 (МГц):' @@ -275,6 +277,12 @@ def setup_gui(params): [sg.Text(SET_RAMP_SRAM_AMP_TEXT, size=(SET_TEXT_WIDTH_NEW,1)), sg.Input(params.get('RampSramAmp', ''), size=(SET_INPUT_WIDTH,1), key='-RampSramAmp-')], + [sg.Button(SET_WAVEFORM_BUTTON_TEXT, key='-UploadWaveform-', disabled_button_color=("Gray22", "Blue"))], + + [sg.Text(SET_WAVEFORM_STATUS_TEXT, size=(SET_TEXT_WIDTH_NEW,1)), + sg.Text(params.get('WaveformStatus', 'waveform.json: не загружен'), + key='-WaveformStatus-', size=(30,2))], + [sg.HSeparator(pad=H_SEPARATOR_PAD)], [sg.Text(SET_AD9833_SECTION_TEXT, size=(SET_TEXT_WIDTH_NEW,1))], diff --git a/waveform.json b/waveform.json new file mode 100644 index 0000000..45bdf9f --- /dev/null +++ b/waveform.json @@ -0,0 +1,259 @@ +[ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 96, + 863, + 1488, + 1920, + 2149, + 2210, + 2170, + 2119, + 2145, + 2319, + 2676, + 3211, + 3874, + 4586, + 5253, + 5787, + 6121, + 6229, + 6126, + 5867, + 5532, + 5214, + 4993, + 4921, + 5013, + 5239, + 5536, + 5818, + 5994, + 5991, + 5767, + 5320, + 4689, + 3946, + 3180, + 2478, + 1907, + 1501, + 1254, + 1122, + 1035, + 909, + 671, + 269, + 314, + 1052, + 1888, + 2740, + 3521, + 4155, + 4593, + 4821, + 4866, + 4781, + 4642, + 4523, + 4486, + 4562, + 4749, + 5012, + 5287, + 5502, + 5585, + 5487, + 5187, + 4702, + 4077, + 3383, + 2698, + 2093, + 1618, + 1293, + 1106, + 1016, + 965, + 890, + 739, + 481, + 115, + 330, + 805, + 1246, + 1591, + 1793, + 1831, + 1711, + 1470, + 1164, + 862, + 625, + 502, + 516, + 663, + 914, + 1221, + 1530, + 1794, + 1981, + 2080, + 2106, + 2094, + 2091, + 2145, + 2293, + 2554, + 2920, + 3362, + 3830, + 4264, + 4605, + 4808, + 4846, + 4719, + 4449, + 4078, + 3656, + 3235, + 2855, + 2536, + 2280, + 2069, + 1872, + 1654, + 1385, + 1046, + 640, + 188, + 271, + 691, + 1024, + 1234, + 1302, + 1231, + 1051, + 807, + 556, + 357, + 256, + 283, + 444, + 723, + 1084, + 1480, + 1863, + 2191, + 2440, + 2602, + 2690, + 2729, + 2749, + 2779, + 2835, + 2917, + 3006, + 3069, + 3065, + 2954, + 2708, + 2318, + 1801, + 1198, + 568, + 18, + 494, + 805, + 923, + 850, + 616, + 278, + 91, + 416, + 633, + 698, + 594, + 331, + 55, + 516, + 997, + 1452, + 1849, + 2176, + 2439, + 2659, + 2858, + 3056, + 3260, + 3457, + 3619, + 3705, + 3674, + 3492, + 3148, + 2658, + 2068, + 1451, + 895, + 485, + 292, + 354, + 665, + 1174, + 1792, + 2402, + 2881, + 3119, + 3039, + 2612, + 1864, + 868, + 264, + 1402, + 2416, + 3204, + 3702 +]