|
| 1 | +import math |
| 2 | +import matplotlib.pyplot as plt |
| 3 | +import numpy as np |
| 4 | +from PIL import Image |
| 5 | +from PIL import Image |
| 6 | +from PIL import ImageFont |
| 7 | +from PIL import ImageDraw |
| 8 | + |
| 9 | + |
| 10 | +def read( |
| 11 | + filename, |
| 12 | + image_filename=None, |
| 13 | + height=480, |
| 14 | + width=640, |
| 15 | + f_min=10, |
| 16 | + f_max=1e8, |
| 17 | + title="", |
| 18 | + subtitle="", |
| 19 | + version="", |
| 20 | + duty_labels=(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9), |
| 21 | + freq_calls=tuple(), |
| 22 | + margin=0.01, |
| 23 | + duty_color=(255, 0, 0), |
| 24 | + freq_color=(0, 255, 0), |
| 25 | + calls_color=(0, 255, 255), |
| 26 | + title_color=(255, 255, 255), |
| 27 | +): |
| 28 | + """Read a one channel logic analyzer raw csv data file and generate a plot visualizing the PWM signal |
| 29 | + captured in the file. Each line of the file is a <time, level> pair indicating the times (in seconds) |
| 30 | + at which the signal transitioned to that level. For example: |
| 31 | + 1.726313020,0 |
| 32 | + 1.726313052,1 |
| 33 | + 1.726313068,0 |
| 34 | + 1.727328804,1 |
| 35 | + """ |
| 36 | + left = 80 |
| 37 | + right = 80 |
| 38 | + bottom = 20 |
| 39 | + top = 60 |
| 40 | + x0 = left |
| 41 | + y0 = top |
| 42 | + y1 = height - bottom |
| 43 | + x1 = width - right |
| 44 | + rising_edge = None |
| 45 | + falling_edge = None |
| 46 | + pixels = np.zeros((height, width, 3), dtype=np.uint8) * 255 |
| 47 | + t0 = None |
| 48 | + t1 = None |
| 49 | + val = None |
| 50 | + with open(filename, "r") as f: |
| 51 | + first = True |
| 52 | + for line in f: # find min and max t, excluding first and last values |
| 53 | + if val is not None: |
| 54 | + if not first: |
| 55 | + if t0 is None or t < t0: |
| 56 | + t0 = t |
| 57 | + if t1 is None or t > t1: |
| 58 | + t1 = t |
| 59 | + else: |
| 60 | + first = False |
| 61 | + t, val = line.split(",") |
| 62 | + try: |
| 63 | + t = float(t) |
| 64 | + val = int(val) |
| 65 | + except ValueError: |
| 66 | + val = None |
| 67 | + print("plotting", t1 - t0, "seconds") |
| 68 | + |
| 69 | + with open(filename, "r") as f: |
| 70 | + pts = 0 |
| 71 | + f_log_max = int(math.log10(f_max)) |
| 72 | + f_log_min = int(math.log10(f_min)) |
| 73 | + f_log_span = f_log_max - f_log_min |
| 74 | + for line in f: |
| 75 | + t, val = line.split(",") |
| 76 | + try: |
| 77 | + t = float(t) |
| 78 | + val = int(val) |
| 79 | + except ValueError: |
| 80 | + val = None |
| 81 | + if val == 1: |
| 82 | + if falling_edge is not None and rising_edge is not None: |
| 83 | + period = t - rising_edge |
| 84 | + frequency = 1 / period |
| 85 | + duty_cycle = (falling_edge - rising_edge) / period |
| 86 | + x = int((x1 - x0) * (t - t0) / (t1 - t0)) + x0 |
| 87 | + y_duty = int((1 - duty_cycle) * (y1 - y0)) + y0 |
| 88 | + y_freq = ( |
| 89 | + int((y1 - y0) * (1 - (math.log10(frequency) - f_log_min) / f_log_span)) |
| 90 | + + y0 |
| 91 | + ) |
| 92 | + x = max(x0, min(x, x1 - 1)) |
| 93 | + y_duty = max(y0, min(y_duty, y1 - 1)) |
| 94 | + y_freq = max(y0, min(y_freq, y1 - 1)) |
| 95 | + pixels[y_duty, x] = duty_color |
| 96 | + pixels[y_freq, x] = freq_color |
| 97 | + pts += 1 |
| 98 | + rising_edge = t |
| 99 | + elif val == 0: |
| 100 | + falling_edge = t |
| 101 | + image = Image.fromarray(pixels) |
| 102 | + draw = ImageDraw.Draw(image) |
| 103 | + draw.text((left - 10, top), "Duty", duty_color, anchor="rt") |
| 104 | + draw.text((0, top), "Calls", calls_color, anchor="lt") |
| 105 | + draw.text((width - right / 2, top), "Freq", freq_color, anchor="mt") |
| 106 | + |
| 107 | + for duty in duty_labels: |
| 108 | + draw.text( |
| 109 | + (left - 10, y0 + (y1 - y0) * (1 - duty)), |
| 110 | + f"{int(100*duty):d}%", |
| 111 | + duty_color, |
| 112 | + anchor="rm", |
| 113 | + ) |
| 114 | + for exponent in range(f_log_min + 1, f_log_max): |
| 115 | + draw.text( |
| 116 | + (width - right / 2, y0 + (y1 - y0) * (1 - (exponent - f_log_min) / (f_log_span))), |
| 117 | + str(10**exponent) + " Hz", |
| 118 | + freq_color, |
| 119 | + anchor="mm", |
| 120 | + ) |
| 121 | + for freq, count in freq_calls: |
| 122 | + draw.text( |
| 123 | + (0, y0 + (y1 - y0) * (1 - (math.log10(freq) - f_log_min) / (f_log_span))), |
| 124 | + f"{count} Hz", |
| 125 | + calls_color, |
| 126 | + anchor="lm", |
| 127 | + ) |
| 128 | + subtitle += f", showing {pts} PWM cycles" |
| 129 | + draw.text((width * 0.5, height * margin), title, title_color, anchor="mm") |
| 130 | + draw.text((width * 0.5, height * 4 * margin), version, title_color, anchor="mm") |
| 131 | + draw.text((width * 0.5, height * 8 * margin), subtitle, title_color, anchor="mm") |
| 132 | + image.show() |
| 133 | + if image_filename is not None: |
| 134 | + image.save(image_filename) |
| 135 | + return image |
0 commit comments