Index: CN0_Python_Scripts/.idea/.gitignore =================================================================== diff -u --- CN0_Python_Scripts/.idea/.gitignore (revision 0) +++ CN0_Python_Scripts/.idea/.gitignore (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ Index: CN0_Python_Scripts/.idea/CN0_Python_Scripts.iml =================================================================== diff -u --- CN0_Python_Scripts/.idea/CN0_Python_Scripts.iml (revision 0) +++ CN0_Python_Scripts/.idea/CN0_Python_Scripts.iml (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file Index: CN0_Python_Scripts/.idea/inspectionProfiles/profiles_settings.xml =================================================================== diff -u --- CN0_Python_Scripts/.idea/inspectionProfiles/profiles_settings.xml (revision 0) +++ CN0_Python_Scripts/.idea/inspectionProfiles/profiles_settings.xml (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,6 @@ + + + + \ No newline at end of file Index: CN0_Python_Scripts/.idea/misc.xml =================================================================== diff -u --- CN0_Python_Scripts/.idea/misc.xml (revision 0) +++ CN0_Python_Scripts/.idea/misc.xml (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file Index: CN0_Python_Scripts/.idea/modules.xml =================================================================== diff -u --- CN0_Python_Scripts/.idea/modules.xml (revision 0) +++ CN0_Python_Scripts/.idea/modules.xml (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file Index: CN0_Python_Scripts/.idea/vcs.xml =================================================================== diff -u --- CN0_Python_Scripts/.idea/vcs.xml (revision 0) +++ CN0_Python_Scripts/.idea/vcs.xml (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file Index: CN0_Python_Scripts/EVAL_CN0359_REV_B.py =================================================================== diff -u -r685d8eb11e2a49f2c8f7a270e379186415594601 -r855524c483c89b2cabf1b0fdb91227d5614b1639 --- CN0_Python_Scripts/EVAL_CN0359_REV_B.py (.../EVAL_CN0359_REV_B.py) (revision 685d8eb11e2a49f2c8f7a270e379186415594601) +++ CN0_Python_Scripts/EVAL_CN0359_REV_B.py (.../EVAL_CN0359_REV_B.py) (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -1,10 +1,4 @@ -import numpy as np import time -from datetime import datetime -import pandas as pd -import os -from serial import Serial -import struct import commands_rev_b as commands # load the Python API for the Analog Devices CN0359 evaluation board @@ -79,54 +73,12 @@ commands.cmd_get_cal_data(cond1) time.sleep(sleep_time) -# Update Coefficient 1 and read response in ASCII format -commands.cmd_setcal(cond1, 1, 3.1) -time.sleep(sleep_time) +# Set Coefficients value 1..12 in order +for idx, value in enumerate(commands.exp_coeff, start=1): + # Update coefficient idx and read response in ASCII format + commands.cmd_setcal(cond1, idx, value) + time.sleep(sleep_time) -# Update Coefficient 2 and read response in ASCII format -commands.cmd_setcal(cond1, 2, 0.006) -time.sleep(sleep_time) - -# Update Coefficient 3 and read response in ASCII format -commands.cmd_setcal(cond1, 3, 0.6) -time.sleep(sleep_time) - -# Update Coefficient 4 and read response in ASCII format -commands.cmd_setcal(cond1, 4, -1.0) -time.sleep(sleep_time) - -# Update Coefficient 5 and read response in ASCII format -commands.cmd_setcal(cond1, 5, 3.1) -time.sleep(sleep_time) - -# Update Coefficient 6 and read response in ASCII format -commands.cmd_setcal(cond1, 6, 0.03) -time.sleep(sleep_time) - -# Update Coefficient 7 and read response in ASCII format -commands.cmd_setcal(cond1, 7, 2.0) -time.sleep(sleep_time) - -# Update Coefficient 8 and read response in ASCII format -commands.cmd_setcal(cond1, 8, 999.0) -time.sleep(sleep_time) - -# Update Coefficient 9 and read response in ASCII format -commands.cmd_setcal(cond1, 9, 1.5) -time.sleep(sleep_time) - -# Update Coefficient 10 and read response in ASCII format -commands.cmd_setcal(cond1, 10, 7.1) -time.sleep(sleep_time) - -# Update Coefficient 11 and read response in ASCII format -commands.cmd_setcal(cond1, 11, 100.11) -time.sleep(sleep_time) - -# Update Coefficient 12 and read response in ASCII format -commands.cmd_setcal(cond1, 12, 100.12) -time.sleep(sleep_time) - # Read Calibration data in ASCII format to confirm the changes were made commands.cmd_get_cal_data(cond1) time.sleep(sleep_time) @@ -136,11 +88,11 @@ time.sleep(sleep_time) # Update Hardware Version Number and read response in ASCII format -commands.cmd_set_hardware_ver(cond1) +commands.cmd_set_hardware_ver(cond1, commands.exp_hw_ver) time.sleep(sleep_time) # Update Serial Number and read response in ASCII format -commands.cmd_set_serial_num(cond1) +commands.cmd_set_serial_num(cond1, commands.exp_sn) time.sleep(sleep_time) # Read Version information in ASCII format to confirm the changes were made @@ -151,4 +103,27 @@ commands.cmd_get_cal_ver_bin(cond1) time.sleep(sleep_time) +######################################################################################################################## +# Below code is for data analysis +# commands.cmd_stop_poll_bin(cond1) +# time.sleep(sleep_time) +# commands.cmd_frequency(cond1,10000.0) +# time.sleep(sleep_time) +# commands.cmd_poll(cond1) +# time.sleep(sleep_time) + +# # Reading Auto polled data for different durations +# read_durations = [30, 60, 120, 240, 480] +# for read_duration in read_durations: +# print("Read Duration:", read_duration) +# commands.cmd_start_poll_bin(cond1) +# time.sleep(sleep_time) +# commands.printTimestamp() +# commands.read_autopoll(cond1, read_duration) +# commands.printTimestamp() +# time.sleep(sleep_time) +# commands.cmd_stop_poll_bin(cond1) +# time.sleep(sleep_time) + + Index: CN0_Python_Scripts/__pycache__/commands_rev_b.cpython-313.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/__pycache__/data_analysis.cpython-313.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/__pycache__/parser.cpython-313.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/autopoll_log.xlsx =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/commands_rev_b.py =================================================================== diff -u -r2b4e5017c953ae23dfbedf1776eec9e492647aa9 -r855524c483c89b2cabf1b0fdb91227d5614b1639 --- CN0_Python_Scripts/commands_rev_b.py (.../commands_rev_b.py) (revision 2b4e5017c953ae23dfbedf1776eec9e492647aa9) +++ CN0_Python_Scripts/commands_rev_b.py (.../commands_rev_b.py) (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -2,6 +2,11 @@ import struct import time from datetime import datetime + +exp_coeff = [3.1, 0.006, 0.6, -1.0, 3.1, 0.03, 2.0, 999.0, 1.5, 7.1, 100.11, 100.12] +exp_fw_ver = "v0.1.0" +exp_hw_ver = "EFGH" +exp_sn = "IJKL" ######################################################################################################################## # This function reads the sensor data and settings # {"poll", cmd_poll}, @@ -136,7 +141,7 @@ data = cond1.ser.read(7) data_count = data_count + 1 # print(data) - # parser.parse_autopoll(data) + parser.parse_autopoll(data) print(data_count) ######################################################################################################################## @@ -208,8 +213,7 @@ ######################################################################################################################## # This function updates the hardware version number in the flash. # {"sethw", cmd_set_hardware_ver}, -def cmd_set_hardware_ver(cond1): - value = "EFGH" +def cmd_set_hardware_ver(cond1, value): cmd = f"sethw {value}\n" cond1.ser.reset_input_buffer() cond1.ser.write(cmd.encode("ascii")) @@ -225,8 +229,7 @@ ######################################################################################################################## # This function updates the serial number in the flash. # {"setsn", cmd_set_serial_num}, -def cmd_set_serial_num(cond1): - value = "IJKL" +def cmd_set_serial_num(cond1, value): cmd = f"setsn {value}\n" cond1.ser.reset_input_buffer() cond1.ser.write(cmd.encode("ascii")) @@ -248,7 +251,9 @@ print("Sent command cmd_get_cal_ver_bin") data = cond1.ser.read(80) print(data) - parser.parse_coeff_and_version(data) + # parser.parse_coeff_and_version(data) + status = parser.verify_coeffs_and_versions(data, exp_coeff, exp_fw_ver,exp_hw_ver,exp_sn,1e-5) + parser.print_verification_results(status) ######################################################################################################################## # This function prints the current data and time Index: CN0_Python_Scripts/data_analysis.py =================================================================== diff -u --- CN0_Python_Scripts/data_analysis.py (revision 0) +++ CN0_Python_Scripts/data_analysis.py (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,135 @@ + +from pathlib import Path +from datetime import datetime +import atexit +from openpyxl import Workbook, load_workbook + +EXCEL_PATH = "autopoll_log.xlsx" + +# Keep workbook open for performance +_wb = None +_ws_map = {} +_pending_writes = 0 + +# Per-sheet headers (include units in the Value column) +HEADER_MAP = { + "Conductivity": ["Number", "Date", "Time", "Value (S/cm)", "Read Count", "Error"], + "Temperature": ["Number", "Date", "Time", "Value (°C)", "Read Count", "Error"], +} + +######################################################################################################################## + +def _headers_match(ws, expected_headers): + """Check if the first row in ws matches the expected headers exactly.""" + row = [ws.cell(row=1, column=c).value for c in range(1, len(expected_headers) + 1)] + return row == expected_headers + +def _write_headers(ws, headers): + """Write headers to the first row.""" + for col, h in enumerate(headers, start=1): + ws.cell(row=1, column=col).value = h + +def _get_or_create_workbook(path: str): + """Open workbook if exists, otherwise create it. Ensure required sheets and headers.""" + global _wb, _ws_map + + if _wb is not None: + return _wb + + p = Path(path) + + if p.exists(): + _wb = load_workbook(p) + else: + _wb = Workbook() # default contains one sheet; we’ll clean it up later + + def ensure_sheet(name: str, headers: list[str]): + # Create or get the sheet + if name in _wb.sheetnames: + ws = _wb[name] + else: + ws = _wb.create_sheet(title=name) + + # If empty or headers mismatch, (re)write headers + # "Empty" heuristic: either only header row exists and it's blank, or sheet is brand new + is_probably_empty = ( + ws.max_row == 1 and + all(ws.cell(row=1, column=c).value is None for c in range(1, len(headers) + 1)) + ) + if is_probably_empty or not _headers_match(ws, headers): + _write_headers(ws, headers) + + return ws + + # Ensure both sheets with their respective headers + _ws_map["Conductivity"] = ensure_sheet("Conductivity", HEADER_MAP["Conductivity"]) + _ws_map["Temperature"] = ensure_sheet("Temperature", HEADER_MAP["Temperature"]) + + # Remove default "Sheet" if unused and not one of our targets + if "Sheet" in _wb.sheetnames and len(_wb.sheetnames) > 2: + ws_default = _wb["Sheet"] + if ( + ws_default.max_row == 1 and + ws_default.max_column == 1 and + ws_default["A1"].value is None + ): + _wb.remove(ws_default) + + return _wb + + +def _next_index(ws): + """Return next sequential Number for this sheet, based on the last used row.""" + # Row 1 is header; data starts at row 2 + if ws.max_row < 2: + return 1 + last_num = ws.cell(row=ws.max_row, column=1).value + try: + return int(last_num) + 1 + except (TypeError, ValueError): + # Fallback based on row count (data rows = max_row - 1) + return (ws.max_row - 1) + 1 + + +def log_to_excel(kind: str, ts: datetime, value: float, read_count: int, error: int, path: str = EXCEL_PATH): + """ + Append a row to the Excel workbook. + + kind: "Conductivity" or "Temperature" + ts: datetime timestamp + value: float + read_count: int + error: int + """ + global _pending_writes + + if kind not in HEADER_MAP: + raise ValueError(f"Unsupported kind '{kind}'. Expected one of: {list(HEADER_MAP.keys())}") + + wb = _get_or_create_workbook(path) + ws = _ws_map[kind] + + n = _next_index(ws) + date_str = ts.date().isoformat() + time_str = ts.time().isoformat() # includes microseconds if present + + ws.append([n, date_str, time_str, float(value), int(read_count), int(error)]) + + _pending_writes += 1 + + # Save periodically to avoid losing data if the script stops unexpectedly + if _pending_writes >= 25: # adjust as you like + wb.save(path) + _pending_writes = 0 + + +def flush_excel(path: str = EXCEL_PATH): + """Force-save any pending writes.""" + global _wb, _pending_writes + if _wb is not None: + _wb.save(path) + _pending_writes = 0 + + +# Auto-save when program exits +atexit.register(flush_excel) Index: CN0_Python_Scripts/modules/__init__.py =================================================================== diff -u --- CN0_Python_Scripts/modules/__init__.py (revision 0) +++ CN0_Python_Scripts/modules/__init__.py (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1 @@ \ No newline at end of file Index: CN0_Python_Scripts/modules/__pycache__/__init__.cpython-312.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/__pycache__/__init__.cpython-313.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/__pycache__/__init__.cpython-38.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/__pycache__/analog_devices_conductivity_board.cpython-312.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/__pycache__/analog_devices_conductivity_board.cpython-313.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/__pycache__/analog_devices_conductivity_board.cpython-38.pyc =================================================================== diff -u Binary files differ Index: CN0_Python_Scripts/modules/analog_devices_conductivity_board.py =================================================================== diff -u --- CN0_Python_Scripts/modules/analog_devices_conductivity_board.py (revision 0) +++ CN0_Python_Scripts/modules/analog_devices_conductivity_board.py (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -0,0 +1,141 @@ +########################################################################### +# +# Copyright (c) 2024-2024 Diality Inc. - All Rights Reserved. +# +# THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN +# WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. +# +# @file analog_devices_conductivity_board.py +# +# @author (original) Christina Heine +# @date (original) 08-October-2024 +# @author (last) Eliza Petersen +# @date (last) 15-November-2024 +# +############################################################################ + +from serial import Serial +import re +import time + +class AnalogDevicesConductivityBoard: + def __init__(self): + self.ser = None + self.run = False + self.pressure = None + self.id = None + self.address = None + + def create_connection(self, port_name, address): + """Creates serial connection to device using known port number.""" + self.address = address.encode('UTF-8') + self.ser = Serial(port_name, baudrate=115200) + + def get_poll(self): + self.ser.write(self.address + b' poll\n') + # poll = str(self.ser.read(385).decode()) #decode by number of characters - this may change + i = 0 + poll = "" + while i <= 21: # decode by number of lines of data messages + current_line_text = str(self.ser.readline().decode()) + poll = poll + current_line_text + i += 1 + + return poll + + def get_poll_more_lines(self, lines_index): + #This function is used for debugging + self.ser.write(self.address + b' poll\n') + # poll = str(self.ser.read(385).decode()) #decode by number of characters - this may change + i = 0 + poll = "" + while i <= lines_index: # decode by number of lines of data messages + current_line_text = str(self.ser.readline().decode()) + poll = poll + current_line_text + i += 1 + + return poll + + def decode_last_poll(self, last_poll, lines_index): + # This function is used for debugging + i = 0 + + while i <= lines_index: # decode by number of lines of data messages + current_line_text = str(self.ser.readline().decode()) + poll = poll + current_line_text + i += 1 + + return poll + + def get_cond(self): + measurements = self.get_poll() + cond = float(re.search('conductivity: (.*)S/cm', measurements).group(1)) * 1000000.00 + return cond + + def set_frequency(self, freq): + freq = str(freq) + byte_freq = freq.encode('UTF-8') + self.ser.write(self.address + b' setfreq ' + byte_freq + b'\n') + time.sleep(1) + self.ser.write(self.address + b' poll\n') + i = 0 + set_freq_message = "" + while i <= 23: # + current_line_text = str(self.ser.readline().decode()) + set_freq_message = set_freq_message + current_line_text + i += 1 + + return set_freq_message + + def set_potential(self, voltage): + voltage = str(voltage) + byte_voltage = voltage.encode('UTF-8') + self.ser.write(self.address + b' setvolt ' + byte_voltage + b'\n') + + time.sleep(0.5) # This delay is critical or else next poll will happen before the set voltage line is + # added to the poll, and won't properly clear out the extra message that adds to the buffer after setting + # potential, affecting the next get poll + + self.ser.write(self.address + b' poll\n') + i = 0 + set_pot_message = "" + while i <= 23: + current_line_text = str(self.ser.readline().decode()) + set_pot_message = set_pot_message + current_line_text + i += 1 + + return set_pot_message + + def get_set_frequency(self): + measurements = self.get_poll() + set_freq = float(re.search('EXC FREQ: (.*)Hz', measurements).group(1)) + return set_freq + + def get_set_potential(self): + measurements = self.get_poll() + set_pot = float(re.search('EXC V: (.*)V', measurements).group(1)) + return set_pot + + def check_frequency(self, target_frequency): + frequency_set = False + set_frequency = self.get_set_frequency() + if set_frequency != target_frequency: + print(f'Frequency incorrectly set! \n' + f'Target frequency: {target_frequency} V\n' + f'Set frequency: {set_frequency} V') + return frequency_set + else: + frequency_set = True + return frequency_set + + def check_potential(self, target_potential): + potential_set = False + set_potential = self.get_set_potential() + if set_potential != target_potential: + print(f'Potential incorrectly set! \n' + f'Target potential: {target_potential} V\n' + f'Set potential: {set_potential} V') + else: + potential_set = True + + return potential_set \ No newline at end of file Index: CN0_Python_Scripts/parser.py =================================================================== diff -u -r685d8eb11e2a49f2c8f7a270e379186415594601 -r855524c483c89b2cabf1b0fdb91227d5614b1639 --- CN0_Python_Scripts/parser.py (.../parser.py) (revision 685d8eb11e2a49f2c8f7a270e379186415594601) +++ CN0_Python_Scripts/parser.py (.../parser.py) (revision 855524c483c89b2cabf1b0fdb91227d5614b1639) @@ -1,6 +1,8 @@ import struct +from datetime import datetime +import data_analysis ######################################################################################################################## # This function parses and prints the received packet during automated polling @@ -24,10 +26,14 @@ _id, value, read_count, error = struct.unpack(' str: - # decode up to first NUL, tolerate missing terminator raw = field_bytes.split(b'\x00', 1)[0] return raw.decode('utf-8', errors='replace').strip() def take_field(max_len: int) -> bytes: - """ - Take up to max_len bytes from packet starting at i. - If fewer than max_len bytes remain, take what's left (tolerate truncated padding). - """ nonlocal i - chunk = packet[i:i + max_len] + chunk = packet[i:i+max_len] i += len(chunk) return chunk - # Read 3 fields, each up to MAX_VERSION_LENGTH, but do NOT require full padding bytes. fw_bytes = take_field(MAX_VERSION_LENGTH) hw_bytes = take_field(MAX_VERSION_LENGTH) sn_bytes = take_field(MAX_VERSION_LENGTH) @@ -84,13 +77,71 @@ hw = parse_ver_field(hw_bytes) sn = parse_ver_field(sn_bytes) - print("Versions:") - print(" FW:", fw) - print(" HW:", hw) - print(" SN:", sn) + # Return structured data + return { + "coeffs": coeffs, + "fw": fw, + "hw": hw, + "sn": sn, + } - # If there’s extra trailing data, show it (optional but useful) - if i < len(packet): - print(f"Trailing bytes ({len(packet)-i}): {packet[i:]}") ######################################################################################################################## + + +def verify_coeffs_and_versions(packet: bytes, + expected_coeffs, + expected_fw, + expected_hw, + expected_sn, + float_tol=1e-5): + parsed = parse_coeff_and_version(packet) + + coeffs_ok = True + coeff_compare = [] + for i, (exp, got) in enumerate(zip(expected_coeffs, parsed["coeffs"]), start=1): + match = abs(exp - got) <= float_tol + coeff_compare.append((i, exp, got, match)) + if not match: + coeffs_ok = False + + fw_ok = (parsed["fw"] == expected_fw) + hw_ok = (parsed["hw"] == expected_hw) + sn_ok = (parsed["sn"] == expected_sn) + + overall_ok = coeffs_ok and fw_ok and hw_ok and sn_ok + + return { + "overall_match": overall_ok, + # Coeffs already carry expected & received per row + "coeff_details": coeff_compare, + # Versions: include both expected and parsed for pretty printing + "versions": { + "fw": {"expected": expected_fw, "received": parsed["fw"], "match": fw_ok}, + "hw": {"expected": expected_hw, "received": parsed["hw"], "match": hw_ok}, + "sn": {"expected": expected_sn, "received": parsed["sn"], "match": sn_ok}, + }, + # Keep the raw parsed payload too (optional) + "parsed": parsed, + } + +######################################################################################################################## + +def print_verification_results(status): + print(f"Overall Match: {status['overall_match']}\n") + + # ---- Coeff Table ---- + print(f"{'Coeff':<8} {'Expected':>12} {'Received':>12} {'Match':>8}") + for idx, exp, got, ok in status["coeff_details"]: + print(f"{idx:<8} {exp:>12.2f} {got:>12.2f} {str(ok):>8}") + + # ---- Version Table ---- + print("\n" + f"{'Field':<12} {'Expected':>12} {'Received':>12} {'Match':>8}") + for field in ("fw", "hw", "sn"): + row = status["versions"][field] + label = field.upper() + print(f"{label:<12} {row['expected']:>12} {row['received']:>12} {str(row['match']):>8}") + + + +######################################################################################################################## \ No newline at end of file