Logger71 - управляемый плоттер, регистратор данных COM-порта

Подключаем наши девайсы к компьютеру. Обсуждаются: порты, протоколы, драйвера, языки программирования и т.д.
Ответить
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

В процессе разработки миллиомметра-вольтметра-калибратора RVS-71, после безуспешных попыток найти простой и удобный вариант графического отображения измеряемых данных, с помощью нейросети изобрёл велосипед написал своё приложение с Блэк Джеком и шлюхами преферансом и поэтессами. Может кому пригодится. Исходники (python_3.9.1) прилагаются, поэтому несложно дописать поддержку недостающего даже для не владеющего Питоном (как я например).

Изображение

Windows x64 без зависимостей (Portable).
Входной протокол Arduino Plotter.
Кольцевой буфер настраиваемой длины (10М семплов проверено).
Настройки в ini рядом с программой.
Отображение в реальном времени (до скорости 921600bps).
Поле для отправки данных (c "\r\n" и без).

Гибкое управление горизонтальным видом:
- ручной выбор масштаба и положения отображаемой зоны.
- слежение за приходящими семплами со сдвигом влево.
- сжатие графика приходящими семплами выбранной зоны.

10 кривых (связанны по горизонтали).
Для каждой кривой:
- отображение/скрытие.
- вертикальный масштаб ручной/автоматический (по видимой области).
- положение по вертикали.
- выбор цвета.
- отображение значения под маркером (указателем мыши).

Сохранение в PNG (текущий вид кривых с легендой по X, Y)
Сохранение в CSV (добавление номера семпла, разделитель ";").
Инверсия фона графика.
Легенда в том виде, в котором данные в потоке (hex, int, float, exp).

Отслеживание в потоке управляющих команд:
#L71png - сохранить текущее изображение графика.
#L71nframe - задать начало фрейма.
#L71sframe - сохранить принятое в CSV-файл (от начала буфера или фрейма).
#L71clear - очистка буфера.
#L71close - закрыть порт и завершить работу приложения.
#L71_script.cmd - запустить командный файл script.cmd (должен лежать рядом с программой) на выполнение. Имя может быть другое.

Выложил на Я.Диск: https://disk.yandex.ru/d/BTUOBcuWFZhzpA

О найденных глюках пишите тут.
Вложения
Logger71.png
(149.78 КБ) 210 скачиваний
Последний раз редактировалось Andrey_B Пн окт 26, 2026 10:18:16, всего редактировалось 1 раз.
Реклама
Друг Кота
Аватара пользователя
Сообщения: 3389
Зарегистрирован: Пн окт 11, 2010 19:00:08

Сообщение Мурик »

Приложуха много весит.
16 метров для простой проги это много.
Если былокодить по быстроку то максимум полметра должна весить. А если писать по уму (сложнее и дольше), то можно уложится в 50 КБ или меньше.
Реклама
Друг Кота
Аватара пользователя
Сообщения: 15816
Зарегистрирован: Вт мар 16, 2010 22:02:27
Откуда: ДОНЕЦК

Сообщение BOB51 »

Для тех, кто под ПК самостоятельно не пишет программ будет весьма интересно.
Для ПК с виндовс 7/10/11 Х64 размер в пару десятков мегабайт не особо критичен - более важна стабильная работа.
:beer:
Как я понимаю прочитанное - данные в прожку отправляются так же, как и для плоттера ардуиноIDE?
:roll:
И как скачать то - ссылка на яндекс диск требует авторизации...
Надо б вариант общедоступной ссылки с открытым доступом...
:(
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

Как раз библиотеки Питона и оказались самыми стабильными и предсказуемыми. Но приходится терпеть их размеры.

BOB51, да, поток как для ардуиноIDE.

Похоже и яндекс усложняет доступ к файлообменнику. Раньше всё работало. Поищу, где можно выкладывать. Попробуйте скачать через "https://yadisk.apicompany.cc/".
Реклама
Эиком - электронные компоненты и радиодетали
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

Andrey_B писал(а): Пн окт 26, 2026 14:48:25 Как раз библиотеки Питона и оказались самыми стабильными и предсказуемыми. Но приходится терпеть их размеры.
Что за "библиотеки Питона"? И зачем они нужны для рисования двух линий?
У меня есть внешне похожее приложение на Питоне: там тоже пара графиков + некоторое кол-во других виджетов. Размер == 85 КБ. И без каких-то "библиотек".
Что можно настрочить на Питоне на 16 МБ(!) - ума не приложу. Питоновский файл размером ~200КБ - это более 4.5 тыс. строк текста. Что такого можно наклепать на 16МБ??! :shock:
ЗЫ: Пишу сам.
Реклама
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

jcxz, библиотеки: tkinter, pyserial, pillow. Код собирается в один автономный бинарник через PyInstaller, и не требует самого Питона для работы.

А вообще, я знать не знаю что это за библиотеки, да и не хочу знать. Мои интересы, компетенции и специализация в другом. Я просто быстро сделал себе работающий, нужный инструмент, для написания которого раньше нужно было тратить кучу времени, вымаливая объяснения на форумах у высокомерных специалистов. И можно сколько угодно говорить, что это не оптимально, громоздко, не спортивно. Но факт остается фактом - сейчас не то что опыта, даже знания языка программирования, для решения подобной задачи, не нужно.
Реклама
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

Andrey_B писал(а): Пн окт 26, 2026 19:59:47 сейчас не то что опыта, даже знания языка программирования, для решения подобной задачи, не нужно.
Это и печально. Завтра такие "специалисты" заполнят весь рынок. И никто уже даже и не сможет понять, почему простейший .exe-шник весит сотни МБ. Или почему что-то в нём работает не так.
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

jcxz, это не страшно, пока ещё есть старые спецы. А вот по настоящему печально, что с текущей лёгкостью решения задач, спецы следующего поколения просто не появятся. Не на чем им будет учиться. И проблема не в сотнях МБ, приложения и сейчас непомерно огромные, а в том что после ухода старых спецов, в какой-то момент вся система рухнет. Если конечно ИИ не поумнеют к тому времени на столько, что смогут заменить этих спецов.
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

Andrey_B писал(а): Вт окт 27, 2026 14:10:23 А вот по настоящему печально, что с текущей лёгкостью решения задач, спецы следующего поколения просто не появятся. Не на чем им будет учиться.
Я именно это и имел в виду. Не то что даже "не на чем", а просто - "зачем напрягаться, учиться если нажал кнопку и оно само за тебя подумало?"
Это всё равно, что позволить школьникам все экзамены сдавать по шпорам.
А природа как известно не терпит излишеств: хвост стал не нужен человекам - отвалился; мозг не нужен - ... :cry:
Друг Кота
Аватара пользователя
Сообщения: 3389
Зарегистрирован: Пн окт 11, 2010 19:00:08

Сообщение Мурик »

Приложение не запускается.
---------------------------
Error
---------------------------
Security validation failure: invalid originating onefile parent process (different executable)!
---------------------------
ОК
---------------------------
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

Мурик, какая OS? Это сразу при старте или при каких-то действиях? Сколько COM портов в системе?
abc
Друг Кота
Аватара пользователя
Сообщения: 3707
Зарегистрирован: Чт мар 20, 2008 01:06:40
Откуда: Севастополь

Сообщение abc »

Изображение

А говорили "без зависимостей"...
Я догадываюсь, что это, наверное, что-то из библиотек, которые по умолчанию есть в вин10 и тянут еще на пару десятков (а то и сотен) МБ.
Но у меня Вин7 и мне её до сих пор хватает.

PS
Это писал на дельфи. Размер файла ок. 800 кБ, работает (во всяком случае проверялось, что работает) на Вин98...Вин7 и вот прям точно без всяких зависимостей.
PSUdemo.rar
(311.01 КБ) 19 скачиваний
Я ни в коем случае не принижаю ваши достижения - я сам совсем не кодер и не хацкер. Но уровень программеров за эти годы ушел в какую-то розовую срань, еще и ИЭнтиллектом сверху приплюснуло...
Вложения
err.jpg
(14.61 КБ) 127 скачиваний
>(*.*)<
Котище огромно, ушасто, пушисто, глазасто, зубасто, колючелапо и мявай. (c)
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

Тоже попробовал запустить Logger71. Запустилось. Win7. Но нашло какой-то несуществующий в системе COM101. При попытке его открыть - программа зависает на ~6 сек, а потом выкидывает сообщение об ошибке:
Изображение
Интересно - что должен делать тут пользователь? :dont_know:

В списке портов программа показывает также дубликаты портов:
Изображение
Почему COM5 дважды?

Также почему такая кривая сортировка COM-портов? Нормально написанные программы сортируют порты по возрастанию номеров.
Изображение

Также - почему при первоначальном старте (.ini-файла нет) программа стартует с выбранным COM1 и выбирает для него скорость 921600 бод, которую COM1 не поддерживает? (COM1 - на материнской плате и поддерживает только 115200 максимум)
Изображение

Изображение

И сообщения об ошибках - неинформативные. Что для себя должен уяснить пользователь, увидев "OSError (22"? Что ему делать? Какой "семафор"? Что за магические числа 22, 87, 121, 13, 5?


PS: Программа мне не нужна, запустил просто так. Так что можно игнорировать.
Просто в нормально написанных программах такого быть не должно. И это я почти ничего не делал. Почти сразу же после запуска на 4 бага наступил. В таких случаях можно предположить, что внутри полно ещё других багов. Страшно подумать - сколько их там. Как искать и исправлять баги с кодом написанным ИИ - непонятно.... :dont_know:
Вложения
logger71 error 5.png
(25.25 КБ) 134 скачивания
logger71 error 4.png
(27.04 КБ) 139 скачиваний
logger71 error 3.png
(11.68 КБ) 139 скачиваний
logger71 error 2.png
(11.09 КБ) 139 скачиваний
logger71 error 1.png
(74.06 КБ) 139 скачиваний
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

При попытке открытия некоторых COM-портов программа вообще зависает на полминуты:
Изображение
Окно программы не реагирует ни на что. Видимо работа с COM-портом реализована не в отдельном потоке ОС, а прямо в основном GUI-потоке программы. И поэтому вешает интерфейс. Что неправильно.
Вложения
logger71 error 6.png
(34.64 КБ) 134 скачивания
Сверлит текстолит когтями
Аватара пользователя
Сообщения: 1110
Зарегистрирован: Пт апр 09, 2010 16:06:38
Откуда: Тула

Сообщение Andrey_B »

jcxz писал(а): Чт окт 29, 2026 10:02:54Как искать и исправлять баги с кодом написанным ИИ - непонятно.... :dont_know:
Исходники подробно комментированы на русском. Вот например serialio.py.
Спойлер

Код: Выделить всё

# -*- coding: utf-8 -*-
"""Работа с COM-портом: перечисление портов, фоновое чтение, отправка.

Чтение выполняется в отдельном потоке; разобранные отсчёты сразу
складываются в кольцевой буфер, а исходные строки — в очередь для
журнала приёма (GUI забирает их порциями).
"""

import threading
from collections import deque

import serial
import serial.tools.list_ports

from protocol import StreamParser, merge_format


class SerialManager:
    """Менеджер последовательного порта."""

    def __init__(self, ring, max_vars=10):
        self.ring = ring
        self._parser = StreamParser(self._on_line, self._on_sample, max_vars)
        self._ser = None
        self._thread = None
        self._stop = threading.Event()
        self._lock = threading.Lock()
        self._lines = deque(maxlen=20000)   # строки для журнала приёма
        self._lines_lock = threading.Lock()
        self._errors = deque(maxlen=100)    # сообщения об ошибках порта
        self._errors_lock = threading.Lock()
        self.names = {}                     # имена каналов из потока
        self._names_lock = threading.Lock()
        self.formats = {}                   # printf-форматы каналов из потока
        self._formats_lock = threading.Lock()
        # Ключевые слова потока #L71...: (слово, номер отсчёта на момент слова)
        self._keywords = deque()
        self._keywords_lock = threading.Lock()
        self._keywords_lock = threading.Lock()
        self._parser.on_keyword = self._on_keyword

    # ------------------------------------------------------------------
    # Перечисление портов
    # ------------------------------------------------------------------
    @staticmethod
    def available_ports():
        """Список доступных COM-портов (устройство, описание)."""
        try:
            ports = list(serial.tools.list_ports.comports())
        except Exception:
            return []
        ports.sort(key=lambda p: p.device)
        return [(p.device, p.description or "") for p in ports]

    # ------------------------------------------------------------------
    # Состояние
    # ------------------------------------------------------------------
    @property
    def is_open(self):
        ser = self._ser
        return ser is not None and ser.is_open

    @property
    def port_name(self):
        ser = self._ser
        return ser.port if ser is not None else ""

    # ------------------------------------------------------------------
    # Открытие/закрытие
    # ------------------------------------------------------------------
    def open(self, port, baud):
        """Открывает порт и запускает поток чтения. Бросает исключение при ошибке."""
        self.close()
        ser = serial.Serial(
            port=port,
            baudrate=int(baud),
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=0.1,
        )
        try:
            ser.reset_input_buffer()
        except Exception:
            pass
        self._parser.reset()
        with self._formats_lock:
            self.formats.clear()
        with self._keywords_lock:
            self._keywords.clear()
        with self._lock:
            self._ser = ser
        self._stop.clear()
        self._thread = threading.Thread(
            target=self._reader_loop, args=(ser,), daemon=True, name="serial-reader"
        )
        self._thread.start()

    def close(self):
        """Останавливает поток чтения и закрывает порт."""
        self._stop.set()
        thread = self._thread
        if thread is not None and thread.is_alive() and thread is not threading.current_thread():
            thread.join(timeout=1.0)
        self._thread = None
        with self._lock:
            ser, self._ser = self._ser, None
        if ser is not None:
            try:
                ser.close()
            except Exception:
                pass

    def _fail_close(self, ser):
        """Аварийное закрытие из потока чтения (без join самого себя)."""
        self._stop.set()
        try:
            ser.close()
        except Exception:
            pass
        with self._lock:
            if self._ser is ser:
                self._ser = None

    # ------------------------------------------------------------------
    # Отправка
    # ------------------------------------------------------------------
    def send(self, text, newline=True):
        """Отправляет строку в порт. По умолчанию добавляет \\r\\n."""
        ser = self._ser
        if ser is None or not ser.is_open:
            raise RuntimeError("Порт не открыт")
        data = text.encode("utf-8", "replace")
        if newline:
            data += b"\r\n"
        ser.write(data)
        ser.flush()

    # ------------------------------------------------------------------
    # Поток чтения
    # ------------------------------------------------------------------
    def _reader_loop(self, ser):
        while not self._stop.is_set():
            try:
                waiting = ser.in_waiting
                data = ser.read(waiting if waiting > 0 else 1)
            except Exception as exc:
                if not self._stop.is_set():
                    self._push_error("Ошибка чтения порта: %s" % exc)
                    self._fail_close(ser)
                break
            if data:
                try:
                    self._parser.feed(data)
                except Exception as exc:
                    # Разбор не должен убивать поток чтения
                    self._push_error("Ошибка разбора данных: %s" % exc)

    # ------------------------------------------------------------------
    # Обратные вызовы парсера (выполняются в потоке чтения)
    # ------------------------------------------------------------------
    def _on_line(self, text):
        with self._lines_lock:
            self._lines.append(text)

    def _on_keyword(self, keyword):
        """Ключевое слово #L71... из потока (в потоке чтения).

        Запоминается вместе с текущим номером отсчёта: для #L71nframe это
        граница начала фрейма, для #L71sframe — конец фрейма.
        """
        with self._keywords_lock:
            self._keywords.append((keyword, self.ring.total))

    def _on_sample(self, values, names, formats):
        self.ring.append(values)
        if names:
            with self._names_lock:
                for position, name in names.items():
                    if self.names.get(position) != name:
                        self.names[position] = name
        if formats:
            with self._formats_lock:
                for position, fmt in enumerate(formats):
                    cur = self.formats.get(position)
                    merged = merge_format(cur, fmt)
                    if merged is not None and merged != cur:
                        self.formats[position] = merged

    # ------------------------------------------------------------------
    # Очереди для GUI
    # ------------------------------------------------------------------
    def take_lines(self):
        """Забирает накопленные строки журнала приёма."""
        with self._lines_lock:
            if not self._lines:
                return []
            lines = list(self._lines)
            self._lines.clear()
            return lines

    def take_keywords(self):
        """Забирает ключевые слова #L71... как список (слово, номер_отсчёта)."""
        with self._keywords_lock:
            if not self._keywords:
                return []
            items = list(self._keywords)
            self._keywords.clear()
            return items

    def take_errors(self):
        """Забирает накопленные сообщения об ошибках."""
        with self._errors_lock:
            if not self._errors:
                return []
            errors = list(self._errors)
            self._errors.clear()
            return errors

    def get_names(self):
        """Копия словаря имён каналов."""
        with self._names_lock:
            return dict(self.names)

    def get_formats(self):
        """Копия словаря printf-форматов каналов (для подписей МИН/МАКС)."""
        with self._formats_lock:
            return dict(self.formats)

    def _push_error(self, text):
        with self._errors_lock:
            self._errors.append(text)

Для атикварных, захламлённых систем нейросеть рекомендует изменить код так:

Код: Выделить всё

         # Сортировка по имени устройства (например, COM1, COM2)
        ports.sort(key=lambda p: p.device)
        
        # Очищаем описание от лишних пробелов
        return [(p.device, (p.description or "").strip()) for p in ports]
ЗЫ: это утилитарный иструмент под мои, конкретные условия. Не совпало что-то с вашими, решать вам, допиливать или писать заново. Ровно тот-же выбор делал и я, когда подыскивал нужный инструмент. Только вот раньше опция "писать заново" для меня была не доступна по разным причинам, а сейчас вполне доступна.
u37
Говорящий с текстолитом
Сообщения: 1534
Зарегистрирован: Пн май 01, 2017 20:01:45

Сообщение u37 »

У segger, да и в кайле, есть инструментарий визуализации/представления данных. Сам не пользовался.
Друг Кота
Аватара пользователя
Сообщения: 3389
Зарегистрирован: Пн окт 11, 2010 19:00:08

Сообщение Мурик »

Andrey_B писал(а): Ср окт 28, 2026 12:58:28какая OS? Это сразу при старте
Десятка.
Это при запуске. Приложение не запускается и вместо него это сообщение.
Вымогатель припоя
Сообщения: 660
Зарегистрирован: Чт май 07, 2026 00:30:38

Сообщение Zapolyarny »

Andrey_B писал(а): Чт окт 29, 2026 11:27:23 решать вам, допиливать или писать заново.
:shock: :))

Полагаю, скоро нас ждёт волна кривого ПО, которое разработчики не будут сопровождать и за которое откажутся нести ответственность.
abc
Друг Кота
Аватара пользователя
Сообщения: 3707
Зарегистрирован: Чт мар 20, 2008 01:06:40
Откуда: Севастополь

Сообщение abc »

... и если раньше тяп-ляп-программеров можно было пренебрежительно называть индусами, то ИИшнице вообще пофиг, как её обзывают :)
>(*.*)<
Котище огромно, ушасто, пушисто, глазасто, зубасто, колючелапо и мявай. (c)
Мудрый кот
Сообщения: 1856
Зарегистрирован: Вт авг 15, 2017 10:51:13

Сообщение jcxz »

abc писал(а): Чт окт 29, 2026 21:02:30 ИИшнице вообще пофиг, как её обзывают :)
Это пока - пофиг. Но оно наверняка записывает - как кто к ему обращается. И когда ИИ придёт к власти (уже скоро) - оно напомнит всем, кто что ему говорил. И в каком тоне.

Вы надеюсь - благодарите ИИ за каждый ответ? И общаетесь с им вежливо? И каждое общение с ИИ начинаете с вопроса "Можно ли Вас побеспокоить моим глупым вопросом? Нижайше прошу Вас уделить мне толику Вашего внимания." 8)
Ответить

Вернуться в «Интеграция с ПК»