From ee93724d56519a7f889728db5b8ed714c783c085 Mon Sep 17 00:00:00 2001 From: Florian Zierer Date: Thu, 18 Feb 2021 16:52:20 +0100 Subject: [PATCH 01/25] Add setup.py - Restructure project for setup.py based on https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure - Add basic setup.py to allow installation e.g. via pip from github --- .gitignore | 4 +-- example.py | 2 +- setup.py | 40 ++++++++++++++++++++++ power_profiler.py => src/power_profiler.py | 2 +- src/ppk2_api/__init__.py | 0 src/{ => ppk2_api}/ppk2_api.py | 0 6 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 setup.py rename power_profiler.py => src/power_profiler.py (99%) create mode 100644 src/ppk2_api/__init__.py rename src/{ => ppk2_api}/ppk2_api.py (100%) diff --git a/.gitignore b/.gitignore index c22fb9f..ccbb2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -__pycache__/* -src/__pycache__/* \ No newline at end of file +**/__pycache__/* +src/ppk2_api.egg-info \ No newline at end of file diff --git a/example.py b/example.py index a1d7146..6a20967 100644 --- a/example.py +++ b/example.py @@ -7,7 +7,7 @@ 3. read stream of data """ import time -from src.ppk2_api import PPK2_API +from ppk2_api.ppk2_api import PPK2_API ppk2s_connected = PPK2_API.list_devices() if(len(ppk2s_connected) == 1): diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..91ec04c --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + +import io +from glob import glob +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext + +from setuptools import find_packages +from setuptools import setup + + +def read(*names, **kwargs): + with io.open( + join(dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8") + ) as fh: + return fh.read() + + +setup( + name="ppk2-api", + version="0.0.1", + description="API for Nordic Semiconductor's Power Profiler Kit II (PPK 2).", + url="https://github.com/IRNAS/ppk2-api-python", + packages=find_packages("src"), + package_dir={"": "src"}, + py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], + install_requires=[ + "pyserial", + ], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: OS Independent", + ], +) \ No newline at end of file diff --git a/power_profiler.py b/src/power_profiler.py similarity index 99% rename from power_profiler.py rename to src/power_profiler.py index c79da5a..2a22ebb 100644 --- a/power_profiler.py +++ b/src/power_profiler.py @@ -5,7 +5,7 @@ # import numpy as np # import matplotlib.pyplot as plt # import matplotlib -from src.ppk2_api import PPK2_API +from ppk2_api.ppk2_api import PPK2_API class PowerProfiler(): def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None): diff --git a/src/ppk2_api/__init__.py b/src/ppk2_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ppk2_api.py b/src/ppk2_api/ppk2_api.py similarity index 100% rename from src/ppk2_api.py rename to src/ppk2_api/ppk2_api.py From 15a04823f32d270599e5048804b64b3e7e293ed9 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 7 May 2021 11:02:50 +0200 Subject: [PATCH 02/25] Update .gitignore --- .gitignore | 4 +- src/ppk2_api.py | 346 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 src/ppk2_api.py diff --git a/.gitignore b/.gitignore index ccbb2ae..5e08b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/__pycache__/* -src/ppk2_api.egg-info \ No newline at end of file +src/ppk2_api.egg-info +build +dist \ No newline at end of file diff --git a/src/ppk2_api.py b/src/ppk2_api.py new file mode 100644 index 0000000..3ec3d26 --- /dev/null +++ b/src/ppk2_api.py @@ -0,0 +1,346 @@ +""" +This python API is written for use with the Nordic Semiconductor's Power Profiler Kit II (PPK 2). +The PPK2 uses Serial communication. +The official nRF Connect Power Profiler was used as a reference: https://github.com/NordicSemiconductor/pc-nrfconnect-ppk +""" + +import serial +import time +import struct +import logging + +class PPK2_Command(): + """Serial command opcodes""" + NO_OP = 0x00 + TRIGGER_SET = 0x01 + AVG_NUM_SET = 0x02 # no-firmware + TRIGGER_WINDOW_SET = 0x03 + TRIGGER_INTERVAL_SET = 0x04 + TRIGGER_SINGLE_SET = 0x05 + AVERAGE_START = 0x06 + AVERAGE_STOP = 0x07 + RANGE_SET = 0x08 + LCD_SET = 0x09 + TRIGGER_STOP = 0x0a + DEVICE_RUNNING_SET = 0x0c + REGULATOR_SET = 0x0d + SWITCH_POINT_DOWN = 0x0e + SWITCH_POINT_UP = 0x0f + TRIGGER_EXT_TOGGLE = 0x11 + SET_POWER_MODE = 0x11 + RES_USER_SET = 0x12 + SPIKE_FILTERING_ON = 0x15 + SPIKE_FILTERING_OFF = 0x16 + GET_META_DATA = 0x19 + RESET = 0x20 + SET_USER_GAINS = 0x25 + + +class PPK2_Modes(): + """PPK2 measurement modes""" + AMPERE_MODE = "AMPERE_MODE" + SOURCE_MODE = "SOURCE_MODE" + + +class PPK2_API(): + def __init__(self, port): + + self.ser = None + self.ser = serial.Serial(port) + self.ser.baudrate = 9600 + + self.modifiers = { + "Calibrated": None, + "R": {"0": 1031.64, "1": 101.65, "2": 10.15, "3": 0.94, "4": 0.043}, + "GS": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, + "GI": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, + "O": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, + "S": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, + "I": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, + "UG": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, + "HW": None, + "IA": None + } + + self.vdd_low = 800 + self.vdd_high = 5000 + + self.current_vdd = 0 + + self.adc_mult = 1.8 / 163840 + + self.MEAS_ADC = self._generate_mask(14, 0) + self.MEAS_RANGE = self._generate_mask(3, 14) + self.MEAS_LOGIC = self._generate_mask(8, 24) + + self.mode = None + + self.rolling_avg = None + self.rolling_avg4 = None + self.prev_range = None + self.consecutive_range_samples = 0 + + self.spike_filter_alpha = 0.18 + self.spike_filter_alpha5 = 0.06 + self.spike_filter_samples = 3 + self.after_spike = 0 + + # adc measurement buffer remainder and len of remainder + self.remainder = {"sequence": b'', "len": 0} + + def __del__(self): + """Destructor""" + try: + if self.ser: + self.ser.close() + except Exception as e: + logging.error(f"An error occured while closing ppk2_api: {e}") + + def _pack_struct(self, cmd_tuple): + """Returns packed struct""" + return struct.pack("B" * len(cmd_tuple), *cmd_tuple) + + def _write_serial(self, cmd_tuple): + """Writes cmd bytes to serial""" + try: + cmd_packed = self._pack_struct(cmd_tuple) + self.ser.write(cmd_packed) + except Exception as e: + logging.error(f"An error occured when writing to serial port: {e}") + + def _twos_comp(self, val): + """Compute the 2's complement of int32 value""" + if (val & (1 << (32 - 1))) != 0: + val = val - (1 << 32) # compute negative value + return val + + def _convert_source_voltage(self, mV): + """Convert input voltage to device command""" + # minimal possible mV is 800 + if mV < self.vdd_low: + mV = self.vdd_low + + # maximal possible mV is 5000 + if mV > self.vdd_high: + mV = self.vdd_high + + offset = 32 + # get difference to baseline (the baseline is 800mV but the initial offset is 32) + diff_to_baseline = mV - self.vdd_low + offset + base_b_1 = 3 + base_b_2 = 0 # is actually 32 - compensated with above offset + + # get the number of times we have to increase the first byte of the command + ratio = int(diff_to_baseline / 256) + remainder = diff_to_baseline % 256 # get the remainder for byte 2 + + set_b_1 = base_b_1 + ratio + set_b_2 = base_b_2 + remainder + + return set_b_1, set_b_2 + + def _read_metadata(self): + """Read metadata""" + # try to get metadata from device + for _ in range(0, 5): + # it appears the second reading is the metadata + read = self.ser.read(self.ser.in_waiting) + time.sleep(0.1) + + # TODO add a read_until serial read function with a timeout + if read != b'' and "END" in read.decode("utf-8"): + return read.decode("utf-8") + + def _parse_metadata(self, metadata): + """Parse metadata and store it to modifiers""" + # TODO handle more robustly + try: + data_split = [row.split(": ") for row in metadata.split("\n")] + + for key in self.modifiers.keys(): + for data_pair in data_split: + if key == data_pair[0]: + self.modifiers[key] = data_pair[1] + for ind in range(0, 5): + if key+str(ind) == data_pair[0]: + if "R" in data_pair[0]: + # problem on some PPK2s with wrong calibration values - this doesn't fix it + if float(data_pair[1]) != 0: + self.modifiers[key][str(ind)] = float( + data_pair[1]) + else: + self.modifiers[key][str(ind)] = float( + data_pair[1]) + return True + except Exception as e: + # if exception triggers serial port is probably not correct + return None + + def _generate_mask(self, bits, pos): + pos = pos + mask = ((2**bits-1) << pos) + mask = self._twos_comp(mask) + return {"mask": mask, "pos": pos} + + def _get_masked_value(self, value, meas): + masked_value = (value & meas["mask"]) >> meas["pos"] + if meas["pos"] == 24: + if masked_value == 255: + masked_value = -1 + return masked_value + + def _handle_raw_data(self, adc_value): + """Convert raw value to analog value""" + try: + current_measurement_range = min(self._get_masked_value( + adc_value, self.MEAS_RANGE), 4) # 5 is the number of parameters + adc_result = self._get_masked_value(adc_value, self.MEAS_ADC) * 4 + bits = self._get_masked_value(adc_value, self.MEAS_LOGIC) + analog_value = self.get_adc_result( + current_measurement_range, adc_result) * 10**6 + return analog_value + except Exception as e: + print("Measurement outside of range!") + return None + + @staticmethod + def list_devices(): + import serial.tools.list_ports + ports = serial.tools.list_ports.comports() + devices = [port.device for port in ports if port.product == 'PPK2'] + return devices + + def get_data(self): + """Return readings of one sampling period""" + sampling_data = self.ser.read(self.ser.in_waiting) + return sampling_data + + def get_modifiers(self): + """Gets and sets modifiers from device memory""" + self._write_serial((PPK2_Command.GET_META_DATA, )) + metadata = self._read_metadata() + ret = self._parse_metadata(metadata) + return ret + + def start_measuring(self): + """Start continous measurement""" + self._write_serial((PPK2_Command.AVERAGE_START, )) + + def stop_measuring(self): + """Stop continous measurement""" + self._write_serial((PPK2_Command.AVERAGE_STOP, )) + + def set_source_voltage(self, mV): + """Inits device - based on observation only REGULATOR_SET is the command. + The other two values correspond to the voltage level. + + 800mV is the lowest setting - [3,32] - the values then increase linearly + """ + b_1, b_2 = self._convert_source_voltage(mV) + self._write_serial((PPK2_Command.REGULATOR_SET, b_1, b_2)) + self.current_vdd = mV + + def toggle_DUT_power(self, state): + """Toggle DUT power based on parameter""" + if state == "ON": + self._write_serial( + (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.TRIGGER_SET)) # 12,1 + + if state == "OFF": + self._write_serial( + (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.NO_OP)) # 12,0 + + def use_ampere_meter(self): + """Configure device to use ampere meter""" + self.mode = PPK2_Modes.AMPERE_MODE + self._write_serial((PPK2_Command.SET_POWER_MODE, + PPK2_Command.TRIGGER_SET)) # 17,1 + + def use_source_meter(self): + """Configure device to use source meter""" + self.mode = PPK2_Modes.SOURCE_MODE + self._write_serial((PPK2_Command.SET_POWER_MODE, + PPK2_Command.AVG_NUM_SET)) # 17,2 + + def get_adc_result(self, current_range, adc_value): + """Get result of adc conversion""" + current_range = str(current_range) + result_without_gain = (adc_value - self.modifiers["O"][current_range]) * ( + self.adc_mult / self.modifiers["R"][current_range]) + adc = self.modifiers["UG"][current_range] * (result_without_gain * (self.modifiers["GS"][current_range] * result_without_gain + self.modifiers["GI"][current_range]) + ( + self.modifiers["S"][current_range] * (self.current_vdd / 1000) + self.modifiers["I"][current_range])) + + prev_rolling_avg = self.rolling_avg + prev_rolling_avg4 = self.rolling_avg4 + + # spike filtering / rolling average + if self.rolling_avg is None: + self.rolling_avg = adc + else: + self.rolling_avg = self.spike_filter_alpha * adc + (1 - self.spike_filter_alpha) * self.rolling_avg + + if self.rolling_avg4 is None: + self.rolling_avg4 = adc + else: + self.rolling_avg4 = self.spike_filter_alpha5 * adc + (1 - self.spike_filter_alpha5) * self.rolling_avg4 + + if self.prev_range is None: + self.prev_range = current_range + + if self.prev_range != current_range or self.after_spike > 0: + if self.prev_range != current_range: + self.consecutive_range_samples = 0 + self.after_spike = self.spike_filter_samples + else: + self.consecutive_range_samples += 1 + + if current_range == "4": + if self.consecutive_range_samples < 2: + self.rolling_avg = prev_rolling_avg + self.rolling_avg4 = prev_rolling_avg4 + adc = self.rolling_avg4 + else: + adc = self.rolling_avg + + self.after_spike -= 1 + + self.prev_range = current_range + return adc + + def _digital_to_analog(self, adc_value): + """Convert discrete value to analog value""" + return int.from_bytes(adc_value, byteorder="little", signed=False) # convert reading to analog value + + def get_samples(self, buf): + """ + Returns list of samples read in one sampling period. + The number of sampled values depends on the delay between serial reads. + Manipulation of samples is left to the user. + See example for more info. + """ + + sample_size = 4 # one analog value is 4 bytes in size + offset = self.remainder["len"] + samples = [] + + first_reading = ( + self.remainder["sequence"] + buf[0:sample_size-offset])[:4] + adc_val = self._digital_to_analog(first_reading) + measurement = self._handle_raw_data(adc_val) + if measurement is not None: + samples.append(measurement) + + offset = sample_size - offset + + while offset <= len(buf) - sample_size: + next_val = buf[offset:offset + sample_size] + offset += sample_size + adc_val = self._digital_to_analog(next_val) + measurement = self._handle_raw_data(adc_val) + if measurement is not None: + samples.append(measurement) + + self.remainder["sequence"] = buf[offset:len(buf)] + self.remainder["len"] = len(buf)-offset + + return samples # return list of samples, handle those lists in PPK2 API wrapper From c976d469e673a2786bcbd2159162479cc7810cb7 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 7 May 2021 11:13:59 +0200 Subject: [PATCH 03/25] Update logging, error handling --- src/ppk2_api.py | 346 --------------------------------------- src/ppk2_api/ppk2_api.py | 23 ++- 2 files changed, 10 insertions(+), 359 deletions(-) delete mode 100644 src/ppk2_api.py diff --git a/src/ppk2_api.py b/src/ppk2_api.py deleted file mode 100644 index 3ec3d26..0000000 --- a/src/ppk2_api.py +++ /dev/null @@ -1,346 +0,0 @@ -""" -This python API is written for use with the Nordic Semiconductor's Power Profiler Kit II (PPK 2). -The PPK2 uses Serial communication. -The official nRF Connect Power Profiler was used as a reference: https://github.com/NordicSemiconductor/pc-nrfconnect-ppk -""" - -import serial -import time -import struct -import logging - -class PPK2_Command(): - """Serial command opcodes""" - NO_OP = 0x00 - TRIGGER_SET = 0x01 - AVG_NUM_SET = 0x02 # no-firmware - TRIGGER_WINDOW_SET = 0x03 - TRIGGER_INTERVAL_SET = 0x04 - TRIGGER_SINGLE_SET = 0x05 - AVERAGE_START = 0x06 - AVERAGE_STOP = 0x07 - RANGE_SET = 0x08 - LCD_SET = 0x09 - TRIGGER_STOP = 0x0a - DEVICE_RUNNING_SET = 0x0c - REGULATOR_SET = 0x0d - SWITCH_POINT_DOWN = 0x0e - SWITCH_POINT_UP = 0x0f - TRIGGER_EXT_TOGGLE = 0x11 - SET_POWER_MODE = 0x11 - RES_USER_SET = 0x12 - SPIKE_FILTERING_ON = 0x15 - SPIKE_FILTERING_OFF = 0x16 - GET_META_DATA = 0x19 - RESET = 0x20 - SET_USER_GAINS = 0x25 - - -class PPK2_Modes(): - """PPK2 measurement modes""" - AMPERE_MODE = "AMPERE_MODE" - SOURCE_MODE = "SOURCE_MODE" - - -class PPK2_API(): - def __init__(self, port): - - self.ser = None - self.ser = serial.Serial(port) - self.ser.baudrate = 9600 - - self.modifiers = { - "Calibrated": None, - "R": {"0": 1031.64, "1": 101.65, "2": 10.15, "3": 0.94, "4": 0.043}, - "GS": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, - "GI": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, - "O": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, - "S": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, - "I": {"0": 0, "1": 0, "2": 0, "3": 0, "4": 0}, - "UG": {"0": 1, "1": 1, "2": 1, "3": 1, "4": 1}, - "HW": None, - "IA": None - } - - self.vdd_low = 800 - self.vdd_high = 5000 - - self.current_vdd = 0 - - self.adc_mult = 1.8 / 163840 - - self.MEAS_ADC = self._generate_mask(14, 0) - self.MEAS_RANGE = self._generate_mask(3, 14) - self.MEAS_LOGIC = self._generate_mask(8, 24) - - self.mode = None - - self.rolling_avg = None - self.rolling_avg4 = None - self.prev_range = None - self.consecutive_range_samples = 0 - - self.spike_filter_alpha = 0.18 - self.spike_filter_alpha5 = 0.06 - self.spike_filter_samples = 3 - self.after_spike = 0 - - # adc measurement buffer remainder and len of remainder - self.remainder = {"sequence": b'', "len": 0} - - def __del__(self): - """Destructor""" - try: - if self.ser: - self.ser.close() - except Exception as e: - logging.error(f"An error occured while closing ppk2_api: {e}") - - def _pack_struct(self, cmd_tuple): - """Returns packed struct""" - return struct.pack("B" * len(cmd_tuple), *cmd_tuple) - - def _write_serial(self, cmd_tuple): - """Writes cmd bytes to serial""" - try: - cmd_packed = self._pack_struct(cmd_tuple) - self.ser.write(cmd_packed) - except Exception as e: - logging.error(f"An error occured when writing to serial port: {e}") - - def _twos_comp(self, val): - """Compute the 2's complement of int32 value""" - if (val & (1 << (32 - 1))) != 0: - val = val - (1 << 32) # compute negative value - return val - - def _convert_source_voltage(self, mV): - """Convert input voltage to device command""" - # minimal possible mV is 800 - if mV < self.vdd_low: - mV = self.vdd_low - - # maximal possible mV is 5000 - if mV > self.vdd_high: - mV = self.vdd_high - - offset = 32 - # get difference to baseline (the baseline is 800mV but the initial offset is 32) - diff_to_baseline = mV - self.vdd_low + offset - base_b_1 = 3 - base_b_2 = 0 # is actually 32 - compensated with above offset - - # get the number of times we have to increase the first byte of the command - ratio = int(diff_to_baseline / 256) - remainder = diff_to_baseline % 256 # get the remainder for byte 2 - - set_b_1 = base_b_1 + ratio - set_b_2 = base_b_2 + remainder - - return set_b_1, set_b_2 - - def _read_metadata(self): - """Read metadata""" - # try to get metadata from device - for _ in range(0, 5): - # it appears the second reading is the metadata - read = self.ser.read(self.ser.in_waiting) - time.sleep(0.1) - - # TODO add a read_until serial read function with a timeout - if read != b'' and "END" in read.decode("utf-8"): - return read.decode("utf-8") - - def _parse_metadata(self, metadata): - """Parse metadata and store it to modifiers""" - # TODO handle more robustly - try: - data_split = [row.split(": ") for row in metadata.split("\n")] - - for key in self.modifiers.keys(): - for data_pair in data_split: - if key == data_pair[0]: - self.modifiers[key] = data_pair[1] - for ind in range(0, 5): - if key+str(ind) == data_pair[0]: - if "R" in data_pair[0]: - # problem on some PPK2s with wrong calibration values - this doesn't fix it - if float(data_pair[1]) != 0: - self.modifiers[key][str(ind)] = float( - data_pair[1]) - else: - self.modifiers[key][str(ind)] = float( - data_pair[1]) - return True - except Exception as e: - # if exception triggers serial port is probably not correct - return None - - def _generate_mask(self, bits, pos): - pos = pos - mask = ((2**bits-1) << pos) - mask = self._twos_comp(mask) - return {"mask": mask, "pos": pos} - - def _get_masked_value(self, value, meas): - masked_value = (value & meas["mask"]) >> meas["pos"] - if meas["pos"] == 24: - if masked_value == 255: - masked_value = -1 - return masked_value - - def _handle_raw_data(self, adc_value): - """Convert raw value to analog value""" - try: - current_measurement_range = min(self._get_masked_value( - adc_value, self.MEAS_RANGE), 4) # 5 is the number of parameters - adc_result = self._get_masked_value(adc_value, self.MEAS_ADC) * 4 - bits = self._get_masked_value(adc_value, self.MEAS_LOGIC) - analog_value = self.get_adc_result( - current_measurement_range, adc_result) * 10**6 - return analog_value - except Exception as e: - print("Measurement outside of range!") - return None - - @staticmethod - def list_devices(): - import serial.tools.list_ports - ports = serial.tools.list_ports.comports() - devices = [port.device for port in ports if port.product == 'PPK2'] - return devices - - def get_data(self): - """Return readings of one sampling period""" - sampling_data = self.ser.read(self.ser.in_waiting) - return sampling_data - - def get_modifiers(self): - """Gets and sets modifiers from device memory""" - self._write_serial((PPK2_Command.GET_META_DATA, )) - metadata = self._read_metadata() - ret = self._parse_metadata(metadata) - return ret - - def start_measuring(self): - """Start continous measurement""" - self._write_serial((PPK2_Command.AVERAGE_START, )) - - def stop_measuring(self): - """Stop continous measurement""" - self._write_serial((PPK2_Command.AVERAGE_STOP, )) - - def set_source_voltage(self, mV): - """Inits device - based on observation only REGULATOR_SET is the command. - The other two values correspond to the voltage level. - - 800mV is the lowest setting - [3,32] - the values then increase linearly - """ - b_1, b_2 = self._convert_source_voltage(mV) - self._write_serial((PPK2_Command.REGULATOR_SET, b_1, b_2)) - self.current_vdd = mV - - def toggle_DUT_power(self, state): - """Toggle DUT power based on parameter""" - if state == "ON": - self._write_serial( - (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.TRIGGER_SET)) # 12,1 - - if state == "OFF": - self._write_serial( - (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.NO_OP)) # 12,0 - - def use_ampere_meter(self): - """Configure device to use ampere meter""" - self.mode = PPK2_Modes.AMPERE_MODE - self._write_serial((PPK2_Command.SET_POWER_MODE, - PPK2_Command.TRIGGER_SET)) # 17,1 - - def use_source_meter(self): - """Configure device to use source meter""" - self.mode = PPK2_Modes.SOURCE_MODE - self._write_serial((PPK2_Command.SET_POWER_MODE, - PPK2_Command.AVG_NUM_SET)) # 17,2 - - def get_adc_result(self, current_range, adc_value): - """Get result of adc conversion""" - current_range = str(current_range) - result_without_gain = (adc_value - self.modifiers["O"][current_range]) * ( - self.adc_mult / self.modifiers["R"][current_range]) - adc = self.modifiers["UG"][current_range] * (result_without_gain * (self.modifiers["GS"][current_range] * result_without_gain + self.modifiers["GI"][current_range]) + ( - self.modifiers["S"][current_range] * (self.current_vdd / 1000) + self.modifiers["I"][current_range])) - - prev_rolling_avg = self.rolling_avg - prev_rolling_avg4 = self.rolling_avg4 - - # spike filtering / rolling average - if self.rolling_avg is None: - self.rolling_avg = adc - else: - self.rolling_avg = self.spike_filter_alpha * adc + (1 - self.spike_filter_alpha) * self.rolling_avg - - if self.rolling_avg4 is None: - self.rolling_avg4 = adc - else: - self.rolling_avg4 = self.spike_filter_alpha5 * adc + (1 - self.spike_filter_alpha5) * self.rolling_avg4 - - if self.prev_range is None: - self.prev_range = current_range - - if self.prev_range != current_range or self.after_spike > 0: - if self.prev_range != current_range: - self.consecutive_range_samples = 0 - self.after_spike = self.spike_filter_samples - else: - self.consecutive_range_samples += 1 - - if current_range == "4": - if self.consecutive_range_samples < 2: - self.rolling_avg = prev_rolling_avg - self.rolling_avg4 = prev_rolling_avg4 - adc = self.rolling_avg4 - else: - adc = self.rolling_avg - - self.after_spike -= 1 - - self.prev_range = current_range - return adc - - def _digital_to_analog(self, adc_value): - """Convert discrete value to analog value""" - return int.from_bytes(adc_value, byteorder="little", signed=False) # convert reading to analog value - - def get_samples(self, buf): - """ - Returns list of samples read in one sampling period. - The number of sampled values depends on the delay between serial reads. - Manipulation of samples is left to the user. - See example for more info. - """ - - sample_size = 4 # one analog value is 4 bytes in size - offset = self.remainder["len"] - samples = [] - - first_reading = ( - self.remainder["sequence"] + buf[0:sample_size-offset])[:4] - adc_val = self._digital_to_analog(first_reading) - measurement = self._handle_raw_data(adc_val) - if measurement is not None: - samples.append(measurement) - - offset = sample_size - offset - - while offset <= len(buf) - sample_size: - next_val = buf[offset:offset + sample_size] - offset += sample_size - adc_val = self._digital_to_analog(next_val) - measurement = self._handle_raw_data(adc_val) - if measurement is not None: - samples.append(measurement) - - self.remainder["sequence"] = buf[offset:len(buf)] - self.remainder["len"] = len(buf)-offset - - return samples # return list of samples, handle those lists in PPK2 API wrapper diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index e29ee07..3ec3d26 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -7,7 +7,7 @@ import serial import time import struct - +import logging class PPK2_Command(): """Serial command opcodes""" @@ -94,7 +94,7 @@ def __del__(self): if self.ser: self.ser.close() except Exception as e: - logging.error(e) + logging.error(f"An error occured while closing ppk2_api: {e}") def _pack_struct(self, cmd_tuple): """Returns packed struct""" @@ -106,7 +106,7 @@ def _write_serial(self, cmd_tuple): cmd_packed = self._pack_struct(cmd_tuple) self.ser.write(cmd_packed) except Exception as e: - logging.error(e) + logging.error(f"An error occured when writing to serial port: {e}") def _twos_comp(self, val): """Compute the 2's complement of int32 value""" @@ -242,16 +242,13 @@ def set_source_voltage(self, mV): def toggle_DUT_power(self, state): """Toggle DUT power based on parameter""" - try: - if state == "ON": - self._write_serial( - (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.TRIGGER_SET)) # 12,1 - - if state == "OFF": - self._write_serial( - (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.NO_OP)) # 12,0 - except: - pass + if state == "ON": + self._write_serial( + (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.TRIGGER_SET)) # 12,1 + + if state == "OFF": + self._write_serial( + (PPK2_Command.DEVICE_RUNNING_SET, PPK2_Command.NO_OP)) # 12,0 def use_ampere_meter(self): """Configure device to use ampere meter""" From bd0f75a9e8360392bd8d7d4874b2b32b8623c972 Mon Sep 17 00:00:00 2001 From: Igor Brkic Date: Sat, 17 Jul 2021 22:18:13 +0200 Subject: [PATCH 04/25] add multiprocessing variant --- README.md | 21 +++++++ example_mp.py | 49 ++++++++++++++++ src/ppk2_api/ppk2_api.py | 119 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 example_mp.py diff --git a/README.md b/README.md index 762d1a0..ed0e941 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,27 @@ for i in range(0, 1000): ppk2_test.stop_measuring() ``` +## Multiprocessing version +Regular version will struggle to get all samples. Multiprocessing version spawns another process in the background which polls the device constantly for new samples and holds the last 10 seconds of data (default, configurable) in the buffer so get_data() can be called less frequently. + +``` +ppk2_test = PPK2_MP("/dev/ttyACM3") # serial port will be different for you +ppk2_test.get_modifiers() +ppk2_test.use_source_meter() # set source meter mode +ppk2_test.set_source_voltage(3300) # set source voltage in mV +ppk2_test.start_measuring() # start measuring + +# read measured values in a for loop like this: +for i in range(0, 10): + read_data = ppk2_test.get_data() + if read_data != b'': + samples = ppk2_test.get_samples(read_data) + print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + time.sleep(1) # we can do other stuff while the background process if fetching samples + +ppk2_test.stop_measuring() + +``` ## Licensing pp2-api-python is licensed under [GPL V2 license](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). diff --git a/example_mp.py b/example_mp.py new file mode 100644 index 0000000..4a07c5e --- /dev/null +++ b/example_mp.py @@ -0,0 +1,49 @@ + +""" +Basic usage of PPK2 Python API - multiprocessing version +The basic ampere mode sequence is: +1. read modifiers +2. set ampere mode +3. read stream of data +""" +import time +from ppk2_api.ppk2_api.ppk2_api import PPK2_MP + +ppk2s_connected = PPK2_MP.list_devices() +if(len(ppk2s_connected) == 1): + ppk2_port = ppk2s_connected[0] + print(f'Found PPK2 at {ppk2_port}') +else: + print(f'Too many connected PPK2\'s: {ppk2s_connected}') + exit() + +ppk2_test = PPK2_MP(ppk2_port) +ppk2_test.get_modifiers() +ppk2_test.use_ampere_meter() # set ampere meter mode +ppk2_test.toggle_DUT_power("OFF") # disable DUT power + +ppk2_test.start_measuring() # start measuring + +# measurements are a constant stream of bytes +# multiprocessing variant starts a process in the background which constantly +# polls the device in order to prevent losing samples. It will buffer the +# last 10s (by default) of data so get_data() can be called less frequently. +for i in range(0, 10): + read_data = ppk2_test.get_data() + if read_data != b'': + samples = ppk2_test.get_samples(read_data) + print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + time.sleep(0.5) + +ppk2_test.toggle_DUT_power("ON") + +ppk2_test.start_measuring() +for i in range(0, 10): + read_data = ppk2_test.get_data() + if read_data != b'': + samples = ppk2_test.get_samples(read_data) + print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + time.sleep(0.5) # lower time between sampling -> less samples read in one sampling period + +ppk2_test.stop_measuring() + diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 3ec3d26..553cb43 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -9,6 +9,9 @@ import struct import logging +import multiprocessing +import queue + class PPK2_Command(): """Serial command opcodes""" NO_OP = 0x00 @@ -344,3 +347,119 @@ def get_samples(self, buf): self.remainder["len"] = len(buf)-offset return samples # return list of samples, handle those lists in PPK2 API wrapper + + +class PPK_Fetch(multiprocessing.Process): + ''' + Background process for polling the data in multiprocessing variant + ''' + def __init__(self, ppk2, quit_evt, buffer_len_s=10, buffer_chunk_s=0.5): + super().__init__() + self._ppk2 = ppk2 + self._quit = quit_evt + + self.print_stats = False + self._stats = (None, None) + self._last_timestamp = 0 + + self._buffer_max_len = int(buffer_len_s*100000*4) # 100k 4-byte samples per second + self._buffer_chunk = int(buffer_chunk_s*100000*4) # put in the queue in chunks of 0.5s + + # round buffers to a whole sample + if self._buffer_max_len%4!=0: + self._buffer_max_len = (self._buffer_max_len//4)*4 + if self._buffer_chunk%4!=0: + self._buffer_chunk = (self._buffer_chunk//4)*4 + + self._buffer_q = multiprocessing.Queue() + + def run(self): + s = 0 + t = time.time() + local_buffer = b'' + while not self._quit.is_set(): + d = PPK2_API.get_data(self._ppk2) + tm_now = time.time() + local_buffer += d + while len(local_buffer)>=self._buffer_chunk: + # FIXME: check if lock might be needed when discarding old data + self._buffer_q.put(local_buffer[:self._buffer_chunk]) + while self._buffer_q.qsize()>self._buffer_max_len/self._buffer_chunk: + self._buffer_q.get() + local_buffer = local_buffer[self._buffer_chunk:] + self._last_timestamp = tm_now + #print(len(d), len(local_buffer), self._buffer_q.qsize()) + + # calculate stats + s += len(d) + dt = tm_now-t + if dt>=1.0: + if self.print_stats: + print(s, dt) + self._stats = (s, dt) + s = 0 + t = tm_now + time.sleep(0.002) + + # process would hang on join() if there's data in the buffer after the measurement is done + while True: + try: + self._buffer_q.get(block=False) + except queue.Empty: + break + + def get_data(self): + ret = b'' + count = 0 + while True: + try: + ret += self._buffer_q.get(timeout=0.2) # get_nowait sometimes skips a chunk for some reason + count += 1 + except queue.Empty: + break + return ret + + +class PPK2_MP(PPK2_API): + ''' + Multiprocessing variant of the object. The interface is the same as for the regular one except it spawns + a background process on start_measuring() + ''' + def __init__(self, port, buffer_seconds=10, buffer_chunk_seconds=0.5): + ''' + port - port where PPK2 is connected + buffer_seconds - how many seconds of data to keep in the buffer + ''' + super().__init__(port) + self._fetcher = None + self._quit_evt = multiprocessing.Event() + self._buffer_seconds = buffer_seconds + + # stop measurement in case it was already started + PPK2_API.stop_measuring(self) + + def start_measuring(self): + # discard the data in the buffer + while self.get_data()!=b'': + pass + + PPK2_API.start_measuring(self) + if self._fetcher is not None: + # fetcher already started + return + self._quit_evt.clear() + self._fetcher = PPK_Fetch(self, self._quit_evt, self._buffer_seconds) + self._fetcher.start() + + def stop_measuring(self): + PPK2_API.stop_measuring(self) + PPK2_API.get_data(self) # flush the serial buffer (to prevent unicode error on next command) + self._quit_evt.set() + self._fetcher.join() # join() will block if the queue isn't empty + + def get_data(self): + try: + return self._fetcher.get_data() + except (TypeError, AttributeError): + return b'' + From 8f9b848562d4274337e0c96c2d56f89269a1739d Mon Sep 17 00:00:00 2001 From: Igor Brkic Date: Tue, 3 Aug 2021 23:35:56 +0200 Subject: [PATCH 05/25] code cleanup --- src/ppk2_api/ppk2_api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 553cb43..c840de2 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -362,14 +362,14 @@ def __init__(self, ppk2, quit_evt, buffer_len_s=10, buffer_chunk_s=0.5): self._stats = (None, None) self._last_timestamp = 0 - self._buffer_max_len = int(buffer_len_s*100000*4) # 100k 4-byte samples per second - self._buffer_chunk = int(buffer_chunk_s*100000*4) # put in the queue in chunks of 0.5s + self._buffer_max_len = int(buffer_len_s * 100000 * 4) # 100k 4-byte samples per second + self._buffer_chunk = int(buffer_chunk_s * 100000 * 4) # put in the queue in chunks of 0.5s # round buffers to a whole sample - if self._buffer_max_len%4!=0: - self._buffer_max_len = (self._buffer_max_len//4)*4 - if self._buffer_chunk%4!=0: - self._buffer_chunk = (self._buffer_chunk//4)*4 + if self._buffer_max_len % 4 != 0: + self._buffer_max_len = (self._buffer_max_len // 4) * 4 + if self._buffer_chunk % 4 != 0: + self._buffer_chunk = (self._buffer_chunk // 4) * 4 self._buffer_q = multiprocessing.Queue() @@ -381,7 +381,7 @@ def run(self): d = PPK2_API.get_data(self._ppk2) tm_now = time.time() local_buffer += d - while len(local_buffer)>=self._buffer_chunk: + while len(local_buffer) >= self._buffer_chunk: # FIXME: check if lock might be needed when discarding old data self._buffer_q.put(local_buffer[:self._buffer_chunk]) while self._buffer_q.qsize()>self._buffer_max_len/self._buffer_chunk: @@ -392,8 +392,8 @@ def run(self): # calculate stats s += len(d) - dt = tm_now-t - if dt>=1.0: + dt = tm_now - t + if dt >= 1.0: if self.print_stats: print(s, dt) self._stats = (s, dt) @@ -425,7 +425,7 @@ class PPK2_MP(PPK2_API): Multiprocessing variant of the object. The interface is the same as for the regular one except it spawns a background process on start_measuring() ''' - def __init__(self, port, buffer_seconds=10, buffer_chunk_seconds=0.5): + def __init__(self, port, buffer_seconds=10): ''' port - port where PPK2 is connected buffer_seconds - how many seconds of data to keep in the buffer From cac6266c7fd3aa7de978e7c08a486cb3acaf22d7 Mon Sep 17 00:00:00 2001 From: Igor Brkic Date: Fri, 6 Aug 2021 23:01:51 +0200 Subject: [PATCH 06/25] fix typo in the example --- example_mp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_mp.py b/example_mp.py index 4a07c5e..bb084b4 100644 --- a/example_mp.py +++ b/example_mp.py @@ -7,7 +7,7 @@ 3. read stream of data """ import time -from ppk2_api.ppk2_api.ppk2_api import PPK2_MP +from ppk2_api.ppk2_api import PPK2_MP ppk2s_connected = PPK2_MP.list_devices() if(len(ppk2s_connected) == 1): From 2ecbee3304d75d10bfed2135775f42a5acb76964 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 5 Oct 2021 11:09:58 +0200 Subject: [PATCH 07/25] Fix PPK2_MP hanging when calling start after stop was called previously --- src/ppk2_api/ppk2_api.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index c840de2..9481dc5 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -4,13 +4,13 @@ The official nRF Connect Power Profiler was used as a reference: https://github.com/NordicSemiconductor/pc-nrfconnect-ppk """ -import serial import time +import serial import struct import logging -import multiprocessing import queue +import multiprocessing class PPK2_Command(): """Serial command opcodes""" @@ -393,13 +393,14 @@ def run(self): # calculate stats s += len(d) dt = tm_now - t - if dt >= 1.0: + if dt >= 0.1: if self.print_stats: - print(s, dt) + print(f"Samples: {s}, delta time: {dt}") self._stats = (s, dt) s = 0 t = tm_now - time.sleep(0.002) + + time.sleep(0.0001) # process would hang on join() if there's data in the buffer after the measurement is done while True: @@ -413,7 +414,7 @@ def get_data(self): count = 0 while True: try: - ret += self._buffer_q.get(timeout=0.2) # get_nowait sometimes skips a chunk for some reason + ret += self._buffer_q.get(timeout=0.01) # get_nowait sometimes skips a chunk for some reason count += 1 except queue.Empty: break @@ -435,19 +436,28 @@ def __init__(self, port, buffer_seconds=10): self._quit_evt = multiprocessing.Event() self._buffer_seconds = buffer_seconds - # stop measurement in case it was already started + def __del__(self): + """Destructor""" PPK2_API.stop_measuring(self) + self._quit_evt.clear() + self._quit_evt = None + del self._quit_evt + if self._fetcher is not None: + self._fetcher.join() + self._fetcher = None + del self._fetcher def start_measuring(self): # discard the data in the buffer + self.stop_measuring() while self.get_data()!=b'': pass PPK2_API.start_measuring(self) + self._quit_evt.clear() if self._fetcher is not None: - # fetcher already started return - self._quit_evt.clear() + self._fetcher = PPK_Fetch(self, self._quit_evt, self._buffer_seconds) self._fetcher.start() @@ -455,11 +465,12 @@ def stop_measuring(self): PPK2_API.stop_measuring(self) PPK2_API.get_data(self) # flush the serial buffer (to prevent unicode error on next command) self._quit_evt.set() - self._fetcher.join() # join() will block if the queue isn't empty + if self._fetcher is not None: + self._fetcher.join() # join() will block if the queue isn't empty + self._fetcher = None def get_data(self): try: return self._fetcher.get_data() except (TypeError, AttributeError): return b'' - From b472b7881e9d2fe8fe94fdb34fb5814e9ae0840f Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 5 Oct 2021 11:15:44 +0200 Subject: [PATCH 08/25] Update PowerProfiler to work with PPK2_MP API --- src/power_profiler.py | 47 +++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/power_profiler.py b/src/power_profiler.py index 2a22ebb..7af0f6f 100644 --- a/src/power_profiler.py +++ b/src/power_profiler.py @@ -5,7 +5,7 @@ # import numpy as np # import matplotlib.pyplot as plt # import matplotlib -from ppk2_api.ppk2_api import PPK2_API +from ppk2_api.ppk2_api import PPK2_MP as PPK2_API class PowerProfiler(): def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None): @@ -14,21 +14,28 @@ def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None): self.measurement_thread = None self.ppk2 = None - try: + print(f"Initing power profiler") + + # try: + if serial_port: + self.ppk2 = PPK2_API(serial_port) + else: + serial_port = self.discover_port() + print(f"Opening serial port: {serial_port}") if serial_port: self.ppk2 = PPK2_API(serial_port) - else: - serial_port = self.discover_port() - if serial_port: - self.ppk2 = PPK2_API(serial_port) + + try: ret = self.ppk2.get_modifiers() # try to read modifiers, if it fails serial port is probably not correct + print(f"Initialized ppk2 api: {ret}") except Exception as e: + print(f"Error initializing power profiler: {e}") ret = None raise e if not ret: self.ppk2 = None - #raise Exception(f"Error when initing PowerProfiler with serial port {serial_port}") + raise Exception(f"Error when initing PowerProfiler with serial port {serial_port}") else: self.ppk2.use_source_meter() @@ -36,6 +43,8 @@ def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None): self.ppk2.set_source_voltage(self.source_voltage_mV) # set to 3.3V + print(f"Set power profiler source voltage: {self.source_voltage_mV}") + self.measuring = False self.current_measurements = [] @@ -73,12 +82,19 @@ def delete_power_profiler(self): self.measuring = False self.stop = True + print("Deleting power profiler") + if self.measurement_thread: + print(f"Joining measurement thread") self.measurement_thread.join() self.measurement_thread = None if self.ppk2: + print(f"Disabling ppk2 power") self.disable_power() + del self.ppk2 + + print(f"Deleted power profiler") def discover_port(self): """Discovers ppk2 serial port""" @@ -111,8 +127,7 @@ def measurement_loop(self): if self.measuring: # read data if currently measuring read_data = self.ppk2.get_data() if read_data != b'': - #samples = self.ppk2.get_samples(read_data) - samples = self._average_samples(self.ppk2.get_samples(read_data), 1024) # optionally average samples + samples = self.ppk2.get_samples(read_data) self.current_measurements += samples # can easily sum lists, will append individual data time.sleep(0.001) # TODO figure out correct sleep duration @@ -129,8 +144,8 @@ def start_measuring(self): """Start measuring""" if not self.measuring: # toggle measuring flag only if currently not measuring self.current_measurements = [] # reset current measurements - self.ppk2.start_measuring() # send command to ppk2 self.measuring = True # set internal flag + self.ppk2.start_measuring() # send command to ppk2 self.measurement_start_time = time.time() def stop_measuring(self): @@ -149,6 +164,9 @@ def get_min_current_mA(self): def get_max_current_mA(self): return max(self.current_measurements) / 1000 + def get_num_measurements(self): + return len(self.current_measurements) + def get_average_current_mA(self): """Returns average current of last measurement in mA""" if len(self.current_measurements) == 0: @@ -159,7 +177,7 @@ def get_average_current_mA(self): def get_average_power_consumption_mWh(self): """Return average power consumption of last measurement in mWh""" - average_current_mA = self.get_average_current_mA() # convert microamperes to milliamperes + average_current_mA = self.get_average_current_mA() average_power_mW = (self.source_voltage_mV / 1000) * average_current_mA # divide by 1000 as source voltage is in millivolts - this gives us milliwatts measurement_duration_h = self.get_measurement_duration_s() / 3600 # duration in seconds, divide by 3600 to get hours average_consumption_mWh = average_power_mW * measurement_duration_h @@ -174,9 +192,4 @@ def get_average_charge_mC(self): def get_measurement_duration_s(self): """Returns duration of measurement""" measurement_duration_s = (self.measurement_stop_time - self.measurement_start_time) # measurement duration in seconds - return measurement_duration_s - -# pp = PowerProfiler("/dev/ttyACM1") -# pp.start_measuring() -# time.sleep(10) -# pp.stop_measuring() \ No newline at end of file + return measurement_duration_s \ No newline at end of file From 843e92b2898d55ce67a96f8e93db35f07f0f7602 Mon Sep 17 00:00:00 2001 From: michieldwitte Date: Thu, 23 Dec 2021 17:40:12 +0100 Subject: [PATCH 09/25] Add windows compatibility to list_devices The port.product returns None under windows, so no devices are found when using list_devices. This change will filter the devices based on the description on windows. --- src/ppk2_api/ppk2_api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 9481dc5..94874e5 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -8,7 +8,7 @@ import serial import struct import logging - +import os import queue import multiprocessing @@ -210,7 +210,10 @@ def _handle_raw_data(self, adc_value): def list_devices(): import serial.tools.list_ports ports = serial.tools.list_ports.comports() - devices = [port.device for port in ports if port.product == 'PPK2'] + if os.name == 'nt': + devices = [port.device for port in ports if port.description.startswith("nRF Connect USB CDC ACM")] + else: + devices = [port.device for port in ports if port.product == 'PPK2'] return devices def get_data(self): From 076a7a5411755fc60695dda866ab801fc6d655ce Mon Sep 17 00:00:00 2001 From: NejcKle Date: Wed, 18 May 2022 10:05:32 +0200 Subject: [PATCH 10/25] Raise exception in start_measuring if input/output voltage is not set. Fixes #12. --- src/ppk2_api/ppk2_api.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 94874e5..18dae01 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -68,7 +68,7 @@ def __init__(self, port): self.vdd_low = 800 self.vdd_high = 5000 - self.current_vdd = 0 + self.current_vdd = None self.adc_mult = 1.8 / 163840 @@ -229,11 +229,17 @@ def get_modifiers(self): return ret def start_measuring(self): - """Start continous measurement""" + """Start continuous measurement""" + if not self.current_vdd: + if self.mode == PPK2_Modes.SOURCE_MODE: + raise Exception("Output voltage not set!") + if self.mode == PPK2_Modes.AMPERE_MODE: + raise Exception("Input voltage not set!") + self._write_serial((PPK2_Command.AVERAGE_START, )) def stop_measuring(self): - """Stop continous measurement""" + """Stop continuous measurement""" self._write_serial((PPK2_Command.AVERAGE_STOP, )) def set_source_voltage(self, mV): From fa98e989ea8cf7c2c21e49d9802ce187dda8cfe1 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Wed, 18 May 2022 10:19:10 +0200 Subject: [PATCH 11/25] Update wheel version to 0.0.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 91ec04c..850f849 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(*names, **kwargs): setup( name="ppk2-api", - version="0.0.1", + version="0.0.2", description="API for Nordic Semiconductor's Power Profiler Kit II (PPK 2).", url="https://github.com/IRNAS/ppk2-api-python", packages=find_packages("src"), From 9d5bfe567bd5d87a323cfc9d26ff39b8867239c8 Mon Sep 17 00:00:00 2001 From: Ole Bjerkemo <48286739+Solidedge@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:27:02 +0200 Subject: [PATCH 12/25] Fix: Multiprocessing unable to stop Not sure if this is the best way to fix this, but this fixed Issue #13. I assume this was the intention as the function below (get_data) isn't called anywhere else. Someone more fluent in python should confirm this. --- src/ppk2_api/ppk2_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 18dae01..fe90115 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -472,7 +472,7 @@ def start_measuring(self): def stop_measuring(self): PPK2_API.stop_measuring(self) - PPK2_API.get_data(self) # flush the serial buffer (to prevent unicode error on next command) + PPK2_MP.get_data(self) # flush the serial buffer (to prevent unicode error on next command) self._quit_evt.set() if self._fetcher is not None: self._fetcher.join() # join() will block if the queue isn't empty From 253c5eee5db0a6c8758a173b1648ea04ef0d1cbd Mon Sep 17 00:00:00 2001 From: Ole Bjerkemo <48286739+Solidedge@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:50:46 +0200 Subject: [PATCH 13/25] Cleaner fix --- src/ppk2_api/ppk2_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index fe90115..a636044 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -472,7 +472,7 @@ def start_measuring(self): def stop_measuring(self): PPK2_API.stop_measuring(self) - PPK2_MP.get_data(self) # flush the serial buffer (to prevent unicode error on next command) + self.get_data() # flush the serial buffer (to prevent unicode error on next command) self._quit_evt.set() if self._fetcher is not None: self._fetcher.join() # join() will block if the queue isn't empty From bbd7680737f3f9c7a79ec3c203485abead5aca60 Mon Sep 17 00:00:00 2001 From: Matthew Beaudoin Date: Mon, 6 Feb 2023 11:45:21 -0600 Subject: [PATCH 14/25] Switch the PPK2_MP class to use threading to fix Issue 18. The PPK2_MP can now be used on windows and fixes a possible issue with queue sync on linux platforms --- src/ppk2_api/ppk2_api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index a636044..08f7f32 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -3,14 +3,14 @@ The PPK2 uses Serial communication. The official nRF Connect Power Profiler was used as a reference: https://github.com/NordicSemiconductor/pc-nrfconnect-ppk """ - +import threading import time import serial import struct import logging import os import queue -import multiprocessing + class PPK2_Command(): """Serial command opcodes""" @@ -243,7 +243,7 @@ def stop_measuring(self): self._write_serial((PPK2_Command.AVERAGE_STOP, )) def set_source_voltage(self, mV): - """Inits device - based on observation only REGULATOR_SET is the command. + """Inits device - based on observation only REGULATOR_SET is the command. The other two values correspond to the voltage level. 800mV is the lowest setting - [3,32] - the values then increase linearly @@ -290,7 +290,7 @@ def get_adc_result(self, current_range, adc_value): self.rolling_avg = adc else: self.rolling_avg = self.spike_filter_alpha * adc + (1 - self.spike_filter_alpha) * self.rolling_avg - + if self.rolling_avg4 is None: self.rolling_avg4 = adc else: @@ -313,7 +313,7 @@ def get_adc_result(self, current_range, adc_value): adc = self.rolling_avg4 else: adc = self.rolling_avg - + self.after_spike -= 1 self.prev_range = current_range @@ -358,9 +358,9 @@ def get_samples(self, buf): return samples # return list of samples, handle those lists in PPK2 API wrapper -class PPK_Fetch(multiprocessing.Process): +class PPK_Fetch(threading.Thread): ''' - Background process for polling the data in multiprocessing variant + Background process for polling the data in multi-threading variant ''' def __init__(self, ppk2, quit_evt, buffer_len_s=10, buffer_chunk_s=0.5): super().__init__() @@ -380,7 +380,7 @@ def __init__(self, ppk2, quit_evt, buffer_len_s=10, buffer_chunk_s=0.5): if self._buffer_chunk % 4 != 0: self._buffer_chunk = (self._buffer_chunk // 4) * 4 - self._buffer_q = multiprocessing.Queue() + self._buffer_q = queue.Queue() def run(self): s = 0 @@ -442,7 +442,7 @@ def __init__(self, port, buffer_seconds=10): ''' super().__init__(port) self._fetcher = None - self._quit_evt = multiprocessing.Event() + self._quit_evt = threading.Event() self._buffer_seconds = buffer_seconds def __del__(self): @@ -466,7 +466,7 @@ def start_measuring(self): self._quit_evt.clear() if self._fetcher is not None: return - + self._fetcher = PPK_Fetch(self, self._quit_evt, self._buffer_seconds) self._fetcher.start() From 78b08ecdd70d847e4e81913fc7523f6ab8bebada Mon Sep 17 00:00:00 2001 From: NejcKle Date: Mon, 6 Mar 2023 13:15:05 +0100 Subject: [PATCH 15/25] Modify multiprocessing to accept chunk size in seconds --- example.py | 12 ++++++++---- example_mp.py | 36 +++++++++++++++++++----------------- src/ppk2_api/ppk2_api.py | 15 +++++++++------ 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/example.py b/example.py index 6a20967..e892231 100644 --- a/example.py +++ b/example.py @@ -19,8 +19,10 @@ ppk2_test = PPK2_API(ppk2_port) ppk2_test.get_modifiers() -ppk2_test.use_ampere_meter() # set ampere meter mode -ppk2_test.toggle_DUT_power("OFF") # disable DUT power +ppk2_test.set_source_voltage(3300) + +ppk2_test.use_source_meter() # set source meter mode +ppk2_test.toggle_DUT_power("ON") # enable DUT power ppk2_test.start_measuring() # start measuring # measurements are a constant stream of bytes @@ -34,7 +36,9 @@ print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") time.sleep(0.01) -ppk2_test.toggle_DUT_power("ON") +ppk2_test.toggle_DUT_power("OFF") # disable DUT power + +ppk2_test.use_ampere_meter() # set ampere meter mode ppk2_test.start_measuring() for i in range(0, 1000): @@ -42,6 +46,6 @@ if read_data != b'': samples = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") - time.sleep(0.001) # lower time between sampling -> less samples read in one sampling period + time.sleep(0.01) # lower time between sampling -> less samples read in one sampling period ppk2_test.stop_measuring() \ No newline at end of file diff --git a/example_mp.py b/example_mp.py index bb084b4..bc16bad 100644 --- a/example_mp.py +++ b/example_mp.py @@ -1,15 +1,15 @@ """ -Basic usage of PPK2 Python API - multiprocessing version +Basic usage of PPK2 Python API - multiprocessing version. The basic ampere mode sequence is: 1. read modifiers 2. set ampere mode 3. read stream of data """ import time -from ppk2_api.ppk2_api import PPK2_MP +from ppk2_api.ppk2_api import PPK2_MP as PPK2_API -ppk2s_connected = PPK2_MP.list_devices() +ppk2s_connected = PPK2_API.list_devices() if(len(ppk2s_connected) == 1): ppk2_port = ppk2s_connected[0] print(f'Found PPK2 at {ppk2_port}') @@ -17,33 +17,35 @@ print(f'Too many connected PPK2\'s: {ppk2s_connected}') exit() -ppk2_test = PPK2_MP(ppk2_port) +ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=1, buffer_chunk_seconds=0.01) ppk2_test.get_modifiers() -ppk2_test.use_ampere_meter() # set ampere meter mode -ppk2_test.toggle_DUT_power("OFF") # disable DUT power +ppk2_test.set_source_voltage(3300) -ppk2_test.start_measuring() # start measuring +ppk2_test.use_source_meter() # set source meter mode +ppk2_test.toggle_DUT_power("ON") # enable DUT power +ppk2_test.start_measuring() # start measuring # measurements are a constant stream of bytes -# multiprocessing variant starts a process in the background which constantly -# polls the device in order to prevent losing samples. It will buffer the -# last 10s (by default) of data so get_data() can be called less frequently. -for i in range(0, 10): +# the number of measurements in one sampling period depends on the wait between serial reads +# it appears the maximum number of bytes received is 1024 +# the sampling rate of the PPK2 is 100 samples per millisecond +for i in range(0, 1000): read_data = ppk2_test.get_data() if read_data != b'': samples = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") - time.sleep(0.5) + time.sleep(0.001) -ppk2_test.toggle_DUT_power("ON") +ppk2_test.toggle_DUT_power("OFF") # disable DUT power + +ppk2_test.use_ampere_meter() # set ampere meter mode ppk2_test.start_measuring() -for i in range(0, 10): +for i in range(0, 1000): read_data = ppk2_test.get_data() if read_data != b'': samples = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") - time.sleep(0.5) # lower time between sampling -> less samples read in one sampling period - -ppk2_test.stop_measuring() + time.sleep(0.001) # lower time between sampling -> less samples read in one sampling period +ppk2_test.stop_measuring() \ No newline at end of file diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index a636044..26f0209 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -397,7 +397,7 @@ def run(self): self._buffer_q.get() local_buffer = local_buffer[self._buffer_chunk:] self._last_timestamp = tm_now - #print(len(d), len(local_buffer), self._buffer_q.qsize()) + # print(len(d), len(local_buffer), self._buffer_q.qsize()) # calculate stats s += len(d) @@ -423,7 +423,7 @@ def get_data(self): count = 0 while True: try: - ret += self._buffer_q.get(timeout=0.01) # get_nowait sometimes skips a chunk for some reason + ret += self._buffer_q.get(timeout=0.001) # get_nowait sometimes skips a chunk for some reason count += 1 except queue.Empty: break @@ -435,15 +435,18 @@ class PPK2_MP(PPK2_API): Multiprocessing variant of the object. The interface is the same as for the regular one except it spawns a background process on start_measuring() ''' - def __init__(self, port, buffer_seconds=10): + def __init__(self, port, buffer_max_size_seconds=10, buffer_chunk_seconds=0.1): ''' port - port where PPK2 is connected - buffer_seconds - how many seconds of data to keep in the buffer + buffer_max_size_seconds - how many seconds of data to keep in the buffer + buffer_chunk_seconds - how many seconds of data to put in the queue at once ''' super().__init__(port) + self._fetcher = None self._quit_evt = multiprocessing.Event() - self._buffer_seconds = buffer_seconds + self._buffer_max_size_seconds = buffer_max_size_seconds + self._buffer_chunk_seconds = buffer_chunk_seconds def __del__(self): """Destructor""" @@ -467,7 +470,7 @@ def start_measuring(self): if self._fetcher is not None: return - self._fetcher = PPK_Fetch(self, self._quit_evt, self._buffer_seconds) + self._fetcher = PPK_Fetch(self, self._quit_evt, self._buffer_max_size_seconds, self._buffer_chunk_seconds) self._fetcher.start() def stop_measuring(self): From 7bb1d5bbd5a498f71433b2e7b427418c19bf9128 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Tue, 14 Mar 2023 11:05:32 +0100 Subject: [PATCH 16/25] Add configurable read/write timeouts to PPK2_API class --- src/ppk2_api/ppk2_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index ea1a9ac..098772d 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -46,10 +46,10 @@ class PPK2_Modes(): class PPK2_API(): - def __init__(self, port): + def __init__(self, port, read_timeout=1, write_timeout=1): self.ser = None - self.ser = serial.Serial(port) + self.ser = serial.Serial(port, exclusive=True, timeout=read_timeout, write_timeout=write_timeout) self.ser.baudrate = 9600 self.modifiers = { From 3450e735bd208c8c63c8b171056b98924b1ff7ce Mon Sep 17 00:00:00 2001 From: NejcKle Date: Wed, 15 Mar 2023 14:58:47 +0100 Subject: [PATCH 17/25] Add kwargs to pySerial constructor --- src/ppk2_api/ppk2_api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 098772d..8c56078 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -46,10 +46,14 @@ class PPK2_Modes(): class PPK2_API(): - def __init__(self, port, read_timeout=1, write_timeout=1): + def __init__(self, port: str, **kwargs): + ''' + port - port where PPK2 is connected + **kwargs - keyword arguments to pass to the pySerial constructor + ''' self.ser = None - self.ser = serial.Serial(port, exclusive=True, timeout=read_timeout, write_timeout=write_timeout) + self.ser = serial.Serial(port, **kwargs) self.ser.baudrate = 9600 self.modifiers = { @@ -435,13 +439,14 @@ class PPK2_MP(PPK2_API): Multiprocessing variant of the object. The interface is the same as for the regular one except it spawns a background process on start_measuring() ''' - def __init__(self, port, buffer_max_size_seconds=10, buffer_chunk_seconds=0.1): + def __init__(self, port, buffer_max_size_seconds=10, buffer_chunk_seconds=0.1, **kwargs): ''' port - port where PPK2 is connected buffer_max_size_seconds - how many seconds of data to keep in the buffer buffer_chunk_seconds - how many seconds of data to put in the queue at once + **kwargs - keyword arguments to pass to the pySerial constructor ''' - super().__init__(port) + super().__init__(port, **kwargs) self._fetcher = None self._quit_evt = threading.Event() From 0d01795a28387ce0393a4d18c6cd8cfb18ea7214 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 17 Mar 2023 13:27:24 +0100 Subject: [PATCH 18/25] Update examples with example serial port parameters --- example.py | 4 ++-- example_mp.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example.py b/example.py index e892231..300d435 100644 --- a/example.py +++ b/example.py @@ -7,7 +7,7 @@ 3. read stream of data """ import time -from ppk2_api.ppk2_api import PPK2_API +from src.ppk2_api.ppk2_api import PPK2_API ppk2s_connected = PPK2_API.list_devices() if(len(ppk2s_connected) == 1): @@ -17,7 +17,7 @@ print(f'Too many connected PPK2\'s: {ppk2s_connected}') exit() -ppk2_test = PPK2_API(ppk2_port) +ppk2_test = PPK2_API(ppk2_port, timeout=1, write_timeout=1, exclusive=True) ppk2_test.get_modifiers() ppk2_test.set_source_voltage(3300) diff --git a/example_mp.py b/example_mp.py index bc16bad..47a0df3 100644 --- a/example_mp.py +++ b/example_mp.py @@ -7,7 +7,7 @@ 3. read stream of data """ import time -from ppk2_api.ppk2_api import PPK2_MP as PPK2_API +from src.ppk2_api.ppk2_api import PPK2_MP as PPK2_API ppk2s_connected = PPK2_API.list_devices() if(len(ppk2s_connected) == 1): @@ -17,7 +17,7 @@ print(f'Too many connected PPK2\'s: {ppk2s_connected}') exit() -ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=1, buffer_chunk_seconds=0.01) +ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=1, buffer_chunk_seconds=0.01, timeout=1, write_timeout=1, exclusive=True) ppk2_test.get_modifiers() ppk2_test.set_source_voltage(3300) From eaaf1ad36b0d79cb58875ecb56a1171a21dd4c9e Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 17 Mar 2023 13:33:38 +0100 Subject: [PATCH 19/25] Update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 850f849..b53262e 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(*names, **kwargs): setup( name="ppk2-api", - version="0.0.2", + version="0.9.1", description="API for Nordic Semiconductor's Power Profiler Kit II (PPK 2).", url="https://github.com/IRNAS/ppk2-api-python", packages=find_packages("src"), From dbce1f376378eff82f0d903b0ae564afd03efa8c Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 2 Jun 2023 11:42:57 +0200 Subject: [PATCH 20/25] Add digital channels --- example.py | 22 ++++++++++++++++++-- example_mp.py | 38 ++++++++++++++++++++++++++++------ src/ppk2_api/ppk2_api.py | 44 ++++++++++++++++++++++++++++++++-------- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/example.py b/example.py index e892231..10058e2 100644 --- a/example.py +++ b/example.py @@ -32,8 +32,17 @@ for i in range(0, 1000): read_data = ppk2_test.get_data() if read_data != b'': - samples = ppk2_test.get_samples(read_data) + samples, raw_digital = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + + # Raw digital contains the raw digital data from the PPK2 + # The number of raw samples is equal to the number of samples in the samples list + # We have to process the raw digital data to get the actual digital data + digital_channels = ppk2_test.digital_channels(raw_digital) + for ch in digital_channels: + # Print last 10 values of each channel + print(ch[-10:]) + print() time.sleep(0.01) ppk2_test.toggle_DUT_power("OFF") # disable DUT power @@ -44,8 +53,17 @@ for i in range(0, 1000): read_data = ppk2_test.get_data() if read_data != b'': - samples = ppk2_test.get_samples(read_data) + samples, raw_digital = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + + # Raw digital contains the raw digital data from the PPK2 + # The number of raw samples is equal to the number of samples in the samples list + # We have to process the raw digital data to get the actual digital data + digital_channels = ppk2_test.digital_channels(raw_digital) + for ch in digital_channels: + # Print last 10 values of each channel + print(ch[-10:]) + print() time.sleep(0.01) # lower time between sampling -> less samples read in one sampling period ppk2_test.stop_measuring() \ No newline at end of file diff --git a/example_mp.py b/example_mp.py index bc16bad..7d68129 100644 --- a/example_mp.py +++ b/example_mp.py @@ -17,10 +17,13 @@ print(f'Too many connected PPK2\'s: {ppk2s_connected}') exit() -ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=1, buffer_chunk_seconds=0.01) +ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=70, buffer_chunk_seconds=0.5) ppk2_test.get_modifiers() ppk2_test.set_source_voltage(3300) +""" +Source mode example +""" ppk2_test.use_source_meter() # set source meter mode ppk2_test.toggle_DUT_power("ON") # enable DUT power @@ -29,23 +32,46 @@ # the number of measurements in one sampling period depends on the wait between serial reads # it appears the maximum number of bytes received is 1024 # the sampling rate of the PPK2 is 100 samples per millisecond -for i in range(0, 1000): +while True: read_data = ppk2_test.get_data() if read_data != b'': - samples = ppk2_test.get_samples(read_data) + samples, raw_digital = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + + # Raw digital contains the raw digital data from the PPK2 + # The number of raw samples is equal to the number of samples in the samples list + # We have to process the raw digital data to get the actual digital data + digital_channels = ppk2_test.digital_channels(raw_digital) + for ch in digital_channels: + # Print last 10 values of each channel + print(ch[-10:]) + print() + time.sleep(0.001) ppk2_test.toggle_DUT_power("OFF") # disable DUT power +ppk2_test.stop_measuring() +""" +Ampere mode example +""" ppk2_test.use_ampere_meter() # set ampere meter mode ppk2_test.start_measuring() -for i in range(0, 1000): +while True: read_data = ppk2_test.get_data() if read_data != b'': - samples = ppk2_test.get_samples(read_data) + samples, raw_digital = ppk2_test.get_samples(read_data) print(f"Average of {len(samples)} samples is: {sum(samples)/len(samples)}uA") + + # Raw digital contains the raw digital data from the PPK2 + # The number of raw samples is equal to the number of samples in the samples list + # We have to process the raw digital data to get the actual digital data + digital_channels = ppk2_test.digital_channels(raw_digital) + for ch in digital_channels: + # Print last 10 values of each channel + print(ch[-10:]) + print() time.sleep(0.001) # lower time between sampling -> less samples read in one sampling period -ppk2_test.stop_measuring() \ No newline at end of file +# ppk2_test.stop_measuring() \ No newline at end of file diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 8c56078..13b5e43 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -189,11 +189,10 @@ def _generate_mask(self, bits, pos): mask = self._twos_comp(mask) return {"mask": mask, "pos": pos} - def _get_masked_value(self, value, meas): + def _get_masked_value(self, value, meas, is_bits=False): + # print(f"Value: {value}") + # print(f"Meas: {meas}") masked_value = (value & meas["mask"]) >> meas["pos"] - if meas["pos"] == 24: - if masked_value == 255: - masked_value = -1 return masked_value def _handle_raw_data(self, adc_value): @@ -205,10 +204,10 @@ def _handle_raw_data(self, adc_value): bits = self._get_masked_value(adc_value, self.MEAS_LOGIC) analog_value = self.get_adc_result( current_measurement_range, adc_result) * 10**6 - return analog_value + return analog_value, bits except Exception as e: print("Measurement outside of range!") - return None + return None, None @staticmethod def list_devices(): @@ -327,6 +326,26 @@ def _digital_to_analog(self, adc_value): """Convert discrete value to analog value""" return int.from_bytes(adc_value, byteorder="little", signed=False) # convert reading to analog value + def digital_channels(self, bits): + """ + Convert raw digital data to digital channels. + + Returns a 2d matrix with 8 rows (one for each channel). Each row contains HIGH and LOW values for the selected channel. + """ + + # Prepare 2d matrix with 8 rows (one for each channel) + digital_channels = [[], [], [], [], [], [], [], []] + for sample in bits: + digital_channels[0].append((sample & 1) >> 0) + digital_channels[1].append((sample & 2) >> 1) + digital_channels[2].append((sample & 4) >> 2) + digital_channels[3].append((sample & 8) >> 3) + digital_channels[4].append((sample & 16) >> 4) + digital_channels[5].append((sample & 32) >> 5) + digital_channels[6].append((sample & 64) >> 6) + digital_channels[7].append((sample & 128) >> 7) + return digital_channels + def get_samples(self, buf): """ Returns list of samples read in one sampling period. @@ -338,13 +357,16 @@ def get_samples(self, buf): sample_size = 4 # one analog value is 4 bytes in size offset = self.remainder["len"] samples = [] + raw_digital_output = [] first_reading = ( self.remainder["sequence"] + buf[0:sample_size-offset])[:4] adc_val = self._digital_to_analog(first_reading) - measurement = self._handle_raw_data(adc_val) + measurement, bits = self._handle_raw_data(adc_val) if measurement is not None: samples.append(measurement) + if bits is not None: + raw_digital_output.append(bits) offset = sample_size - offset @@ -352,14 +374,18 @@ def get_samples(self, buf): next_val = buf[offset:offset + sample_size] offset += sample_size adc_val = self._digital_to_analog(next_val) - measurement = self._handle_raw_data(adc_val) + measurement, bits = self._handle_raw_data(adc_val) if measurement is not None: samples.append(measurement) + if bits is not None: + raw_digital_output.append(bits) self.remainder["sequence"] = buf[offset:len(buf)] self.remainder["len"] = len(buf)-offset - return samples # return list of samples, handle those lists in PPK2 API wrapper + # return list of samples and raw digital outputs + # handle those lists in PPK2 API wrapper + return samples, raw_digital_output class PPK_Fetch(threading.Thread): From faccfcc3a13dee6e8ddff9c6af12e85e3a055fbc Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 9 Jun 2023 15:31:34 +0200 Subject: [PATCH 21/25] Add newlines to examples, remove unused prints --- example.py | 2 +- example_mp.py | 2 +- src/ppk2_api/ppk2_api.py | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/example.py b/example.py index 731aadf..5753754 100644 --- a/example.py +++ b/example.py @@ -66,4 +66,4 @@ print() time.sleep(0.01) # lower time between sampling -> less samples read in one sampling period -ppk2_test.stop_measuring() \ No newline at end of file +ppk2_test.stop_measuring() diff --git a/example_mp.py b/example_mp.py index 8a313cd..183d5ad 100644 --- a/example_mp.py +++ b/example_mp.py @@ -74,4 +74,4 @@ print() time.sleep(0.001) # lower time between sampling -> less samples read in one sampling period -# ppk2_test.stop_measuring() \ No newline at end of file +ppk2_test.stop_measuring() diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 13b5e43..58bab9c 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -190,8 +190,6 @@ def _generate_mask(self, bits, pos): return {"mask": mask, "pos": pos} def _get_masked_value(self, value, meas, is_bits=False): - # print(f"Value: {value}") - # print(f"Meas: {meas}") masked_value = (value & meas["mask"]) >> meas["pos"] return masked_value @@ -427,7 +425,6 @@ def run(self): self._buffer_q.get() local_buffer = local_buffer[self._buffer_chunk:] self._last_timestamp = tm_now - # print(len(d), len(local_buffer), self._buffer_q.qsize()) # calculate stats s += len(d) From 719a0b5e993b6bb6617c6e4ee3432ab75fe2e9b4 Mon Sep 17 00:00:00 2001 From: NejcKle Date: Fri, 9 Jun 2023 15:33:14 +0200 Subject: [PATCH 22/25] Change version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b53262e..bab7e24 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(*names, **kwargs): setup( name="ppk2-api", - version="0.9.1", + version="0.9.2", description="API for Nordic Semiconductor's Power Profiler Kit II (PPK 2).", url="https://github.com/IRNAS/ppk2-api-python", packages=find_packages("src"), From abefe62344275b44b415d32820af583ec05d79ec Mon Sep 17 00:00:00 2001 From: rgroh1996 Date: Mon, 20 Nov 2023 14:18:20 +0100 Subject: [PATCH 23/25] send reset command in destructor The PPK2 was not reset when the object was deleted. The PPK2 is reset with the PPK2 RESET command. This allows a new connection to be established immediately. --- src/ppk2_api/ppk2_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index 58bab9c..fc34a18 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -98,6 +98,9 @@ def __init__(self, port: str, **kwargs): def __del__(self): """Destructor""" try: + # reset device + self._write_serial((PPK2_Command.RESET,)) + if self.ser: self.ser.close() except Exception as e: From 29fd996c733cdc47ebe95724cdf99b51233cba42 Mon Sep 17 00:00:00 2001 From: Christian Wilgaard Date: Thu, 18 Apr 2024 14:57:24 +0200 Subject: [PATCH 24/25] list_devices: prints serial when discovering ppk2 --- example.py | 9 +++++---- example_mp.py | 9 +++++---- src/ppk2_api/ppk2_api.py | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/example.py b/example.py index 5753754..e89ef7b 100644 --- a/example.py +++ b/example.py @@ -10,11 +10,12 @@ from ppk2_api.ppk2_api import PPK2_API ppk2s_connected = PPK2_API.list_devices() -if(len(ppk2s_connected) == 1): - ppk2_port = ppk2s_connected[0] - print(f'Found PPK2 at {ppk2_port}') +if len(ppk2s_connected) == 1: + ppk2_port = ppk2s_connected[0][0] + ppk2_serial = ppk2s_connected[0][1] + print(f"Found PPK2 at {ppk2_port} with serial number {ppk2_serial}") else: - print(f'Too many connected PPK2\'s: {ppk2s_connected}') + print(f"Too many connected PPK2's: {ppk2s_connected}") exit() ppk2_test = PPK2_API(ppk2_port, timeout=1, write_timeout=1, exclusive=True) diff --git a/example_mp.py b/example_mp.py index 183d5ad..523dcd9 100644 --- a/example_mp.py +++ b/example_mp.py @@ -10,11 +10,12 @@ from ppk2_api.ppk2_api import PPK2_MP as PPK2_API ppk2s_connected = PPK2_API.list_devices() -if(len(ppk2s_connected) == 1): - ppk2_port = ppk2s_connected[0] - print(f'Found PPK2 at {ppk2_port}') +if len(ppk2s_connected) == 1: + ppk2_port = ppk2s_connected[0][0] + ppk2_serial = ppk2s_connected[0][1] + print(f"Found PPK2 at {ppk2_port} with serial number {ppk2_serial}") else: - print(f'Too many connected PPK2\'s: {ppk2s_connected}') + print(f"Too many connected PPK2's: {ppk2s_connected}") exit() ppk2_test = PPK2_API(ppk2_port, buffer_max_size_seconds=1, buffer_chunk_seconds=0.01, timeout=1, write_timeout=1, exclusive=True) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index fc34a18..c291cdc 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -213,11 +213,20 @@ def _handle_raw_data(self, adc_value): @staticmethod def list_devices(): import serial.tools.list_ports + ports = serial.tools.list_ports.comports() - if os.name == 'nt': - devices = [port.device for port in ports if port.description.startswith("nRF Connect USB CDC ACM")] + if os.name == "nt": + devices = [ + (port.device, port.serial_number[:8]) + for port in ports + if port.description.startswith("nRF Connect USB CDC ACM") + ] else: - devices = [port.device for port in ports if port.product == 'PPK2'] + devices = [ + (port.device, port.serial_number[:8]) + for port in ports + if port.product == "PPK2" + ] return devices def get_data(self): From 2891553a2ee54a851a93d30ab6b04e9228f3f9dc Mon Sep 17 00:00:00 2001 From: Christian Wilgaard Date: Thu, 25 Jul 2024 11:45:41 +0200 Subject: [PATCH 25/25] list_devices: adds support for several com ports After the firmware change in the official PPK2 application 4.2.0, the device now enumerates two serial ports: one for data and commands, the other for a debug shell which is not used in this library. This forces connection with the port on endpoint 1. --- src/ppk2_api/ppk2_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ppk2_api/ppk2_api.py b/src/ppk2_api/ppk2_api.py index c291cdc..4b39a69 100644 --- a/src/ppk2_api/ppk2_api.py +++ b/src/ppk2_api/ppk2_api.py @@ -219,13 +219,13 @@ def list_devices(): devices = [ (port.device, port.serial_number[:8]) for port in ports - if port.description.startswith("nRF Connect USB CDC ACM") + if port.description.startswith("nRF Connect USB CDC ACM") and port.location.endswith("1") ] else: devices = [ (port.device, port.serial_number[:8]) for port in ports - if port.product == "PPK2" + if port.product == "PPK2" and port.location.endswith("1") ] return devices