Удалённое оборудование

В основе диспетчера использован обмен переменными по сети между расчётными задачами. Используя тот же принцип, можно обмениваться данными с реальным оборудованием, как будто это задача, работающая в реальном времени.

Разберём этот случай на примере разработанного в аэрокосмическом университете стенда для обучения проектированию систем ориентации космических аппаратов.

По этой теме была написана статья в журнале «Приборы и техника эксперимента»; она находится в репозитории, для детальной информации об этом стенде, пожалуйста, прочитайте её.

В статье описан простой и недорогой, лёгкий в повторении и работе стенд для удалённого обучения проектированию систем ориентации космических аппаратов. Стенд построен на микрокомпьютере Raspberry Pi, датчиковый состав стенда аналогичен аппарату формата Кубсат. Динамические параметры могут в некоторых пределах варьироваться. Алгоритм управления выполняется удалённо на компьютере студента, в среде Python. Возможности стенда позволяют изучить и отладить базовые алгоритмы управления угловой скоростью и положением космического аппарата для одномерного случая.

Структурная схема представлена на рисунке; она полностью повторяет структуру системы управления с обратной связью. Система разделена на две части: клиентскую и серверную.

Структурная схема

Первую, ядро системы управления (регулятор, устройство сравнения, наблюдатель и задатчик), должен реализовать студент на своём компьютере в виде управляющей задачи.

Серверная часть, выполняющая команды клиента, содержит объект управления (тело аппарата), исполнительные органы, датчики и управляющий всем этим интерфейсный контроллер. Код сервера представляет собой типовую задачу, просто в коде имеются обращения к реальной аппаратуре.

Датчиковый состав стенда соответствует космическим аппаратам формата Кубсат: гироскоп и акселерометр на MPU-9250, магнитометры AK8963 и HMC5883L (между собой отличаются точностью и уровнем шума). Гироскоп используется для измерения угловой скорости, а магнитометры - для ориентирования относительно магнитного поля Земли.

Датчики выдают трехкомпонентные вектора в приборной системе координат, и именно ими мы будем обмениваться между управляющей задачей и задачей работающей на микрокомпьютере стенда.

Файл с базой данных (и списком задач) должны быть идентичны на управляющем компьютере и на RaspberryPi:

class DataBase():
    # --- общие переменные ---
    t = 0.   # время
    dt = 0.1 # шаг задач
    tmax = 20.  # время моделирования
    cmd = 0 # команда всем задачам

    u = 1.0 # float [-1..+1] - управление вентиляторами (коэфф. ШИМ)
    # трехкомпонентные вектора из float
    w =  [0.]*3 # wx, wy, wz [deg/s]
    a =  [0.]*3 # ax, ay, az [g]
    m =  [0.]*3 # mx, my, mz [mT]
    m2 = [0.]*3 # m2x, m2y, m2z [mT] (QMC5883)

# Список задач
Tasks = {
    'Controller':{'Keys':'t,cmd,dt,u,w,m,m2',
                  'Addr':('192.168.1.15', 6500)},
    'RemoteSys': {'Keys':'t,cmd,dt,u,w,m,m2',
                  'Addr':('192.168.1.5', 6523)},
    }

Из особенностей здесь мы видим только жесткое задание адресов в списке задач, все остальное полностью стандартное.

Адреса необходимо указывать, потому что у нас стенд имеет свой собственный адрес - теперь задачи работают на разных машинах.

Примечание

Адреса должны быть доступны между собой.

Например, если мы укажем в задаче, которая расположена на нашем персональном компьютере (Controller) его локальный адрес 127.0.0.1 (localhost), то возникнет ошибка доступа (OSError: [Errno 22] Invalid argument).

Дело в том, что из внутренней сети localhost невозможно найти путь до 192.168.1.5 (см. обсуждение проблемы на StackOverflow). Нужно указывать полные сетевые адреса для всех задач.

Понятно, что адреса могут быть и в глобальной сети, но они должны быть жёстко прикреплены к удалённой системе (белый IP).

Рассмотрим сначала управляющую задачу.

Управляющая задача

import libKernel
from libGraph2D import Plot2D
import time
import db

class Controller(libKernel.TaskTemplate):
    """ управление """
    def Setup(self):
        self.Dt = 0.0
        self.plot1 = Plot2D(DB=self)
        self.plot1.Setup('t', [['w'], ['Dt']]) # ['m', 'm2'],

    def Initialize(self):
        self.plot1.Initialize() # инициализируем график
        self.t0 = time.time()
        self.tStart = time.time()

    def Run(self):
        """ просто отображаем данные датчиков """
        t = time.time()
        self.Dt = t - self.t0
        self.plot1.Run()
        self.t0 = t

    def Finalize(self):
        self.plot1.Finalize()
        print('Общее время работы:', time.time() - self.tStart)

#% Главный цикл
model = Controller(TaskList=db.Tasks, DB=db.DataBase(),
                   isSheduler=True, isRealTime=True)
model.Manager.Loop()

Эта задача выполняет две функции:

  1. Она отображает данные измерений гироскопа (угловую скорость), каждую итерацию по времени.

Как строить графики подробно рассмотрено в примере 2.3 (см. задачу tPlot2D.py, я просто взял её за основу).

  1. Производит расчет фактического интервала времени, который получается при обмене данными с удалённой системой.

Смотрите, в классе Controller заводим атрибут Dt (метод Setup) и указываем его при создании экземпляра графика, чтобы тот его отрисовывал.

Затем в методе Run берётся текущий момент времени, рассчитывается Dt и отправляется на график.

Время начала эксперимента t0 запоминается при инициализации задачи, а затем от него рассчитывается разница между текущим и начальным временем.

Также обратите внимание, что при создании объекта задачи (model) указано, что задача работает в реальном времени (isRealTime=True). При этом диспетчер привязывает модельное время к реальному времени.

Пример графика приведён ниже. Видим, что шаг в реальном времени примерно 0,1 сек. В текущей версии программы меньше этого интервала мне получить не удалось. И это время получено при обмене в локальной сети, при обмене через интернет оно получалось порядка 0,25 с.

График

Задача на удалённой системе

Код:

import libKernel9

import pigpio
from mpu9250 import mpu9250
import qmc5883l as qmc

import db

clip = lambda n, minn, maxn: max(min(maxn, n), minn)

# Fan control pins number (Broadcom) https://pinout.xyz/#s
cwPin = 13  # clockwise fan
ccPin = 12 # conuterclockwise fan
freq = 1000 # frequency, Hz
MEG = 1000000 # 1M, for duty cycle

class RemoteSys(libKernel9.TaskTemplate):
    """  Интерфейс к датчикам и вентиляторам стенда """
    def Setup(self):
        self.pi = pigpio.pi() # pi accesses the local Pi's GPIO
        if not self.pi.connected:
            print('[!] can\'t connect with pigpio!')
            exit()

        self.pi.hardware_PWM(ccPin, freq, 0) # stop fans...
        self.pi.hardware_PWM(cwPin, freq, 0)
        self.imu = mpu9250()
        self.compass = qmc.QMC5883L()

    def Initialize(self):
        self.pi.hardware_PWM(ccPin, freq, 0) # stop fans...
        self.pi.hardware_PWM(cwPin, freq, 0)

    def Run(self):
        """ работа с датчиками """
        self.pi.hardware_PWM(ccPin, freq, int(MEG*clip(-self.u, 0., 1.)))
        self.pi.hardware_PWM(cwPin, freq, int(MEG*clip(self.u, 0., 1.)))
        self.w = list(self.imu.gyro)
        self.a = list(self.imu.accel)
        self.m = list(self.imu.mag)
        self.m2 = list(self.compass.measure()) # m2x, m2y, m2z [mT] (QMC5883)

    def Finalize(self):
        self.pi.hardware_PWM(ccPin, freq, 0) # stop fans...
        self.pi.hardware_PWM(cwPin, freq, 0)


#% Главный цикл
model = RemoteSys(TaskList=db.Tasks, DB=db.DataBase())
model.Manager.Loop()

В этом коде есть несколько моментов, связанных с работой с реальным железом RaspberryPi: 1. Библиотека pigpio, которая работает с дискретными выводами и ШИМ выходами RaspberryPi. 2. Библиотека датчика MPU-9250 (акселерометр, гироскоп, магнитометр) 3. Библиотека работы с магнитометром QMC5883L.

В методе Setup создаются объекты, которые работают с датчиками и ШИМ. Соответственно, при инициализации уровень ШИМ сигнала устанавливается равным нулю (чтобы остановить вентиляторы), при финализации происходит тоже самое.

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

Симулятор удалённой системы

Предыдущий пример вы не сможете запустить, если у вас не будет включенного стенда. Можно конечно написать мне на емейл, и я его включу чтобы вы могли попробовать. Но проще всего использовать симулятор - задачу, которая симулирует поведение удалённой системы - стенда.

Описание - в работе..