Вероятно, голова у робота будет самым “чувствительным” органом. Я понимаю, что это не слишком практично, но так уж наши мозги заточены в ключе антропоморфизма: пытаемся даже набор железок и пластика наделить характерными для себя свойствами. Ладно уж, пускай…


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

Это необязательное условие, но чего бы и не облегчить себе жизнь, раз это представляется возможным. Мегамозга у робота еще нет и когда он появится – хрен знает. Соответственно, обмен данными через него пока недоступен. Зато доступен прямой обмен данными внутри контроллера. Он пока и используется.

Кстати, кажется таки появился хороший кандидат на должность мега-мозга: nVidia Jetson Nano. Пока оно еще в стадии вдумчивого изучения, но пока я не вижу никаких подводных камней… Фактически, весь нужный роботу функционал, прямо из коробки.


Звуковые сенсоры

Как уже упоминалось, в качестве сенсоров звука, используются два датчика VMA309 , расположенные по бокам от носа:

В рамках их обслуживания, пришлось решить ряд не очень очевидных на первый взгляд, проблем. И главная из них… А, что считать, собственно, звуком/шумом? Казалось бы, странный вопрос, но…

Вот, предположим, робот находится ночью в тишине дома. Гробовая тишина спального района на окраине Хьюстона. Все спят. Опоссумы, белки, компьютер на столе…

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

В этом случае, даже пролетевший мимо вашего спящего уха комар, уже будет реветь, как стартующий Saturn-5 c макаками на борту, которые узнали, что возвращать их потом обратно на Землю никто не собирается.

И теперь, предположим, ту же ситуацию, но уже днем. В доме и вокруг кипит бурная активность: белки дерутся из за шишки на крыше, Кот швыряется об стену за каким-то только ему одному известным лядом, Пес скребется в дверь побрехать на белок и сбежать от Кота, дочь пришла в гости поорать как все вокруг “уи-и-и-и!”, из кабинета играет музыка, я громогласно требую кофе и зрелищ, жена пытается призвать все это к порядку, иногда за окном проезжает машина. Над крышей летят пароходы, плывут самолеты, орут самураи… Короче, обычный такой день.

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

Пока запомним этот пример в голове и посмотрим на то, как работает звуковой сенсор у робота…


У сенсора есть аналоговый и цифровой выходы. И есть некий подстроечный резистор, который может задавать некое пороговое значение сигнала:

Если уровень шума (давление звука на мембрану микрофона) будет ниже этого порога, цифровой выход будет показывать 0. Если выше этого порога, то цифровой выход будет показывать 1.

Аналоговый выход намного интереснее. Он выдает некий сигнал, снятый с обмотки микрофона напрямую и лишь слегка адаптированный для практического применения. Т.е., в зависимости от колебаний мембраны микрофона, аналоговый сигнал будет так же менять свое значение в некотором пределе. В этом случае, подстроечный резистор задает только некую “нулевую черту”, вокруг которой этот сигнал колбасится в плюс или в минус:

Что происходит со стороны контроллера: он получает на входе, либо 0/1 от цифрового выхода сенсора, либо некое значение в пределах от 0 до 1023 от аналогового выхода сенсора. Т.о. можно уговорить его, что-то делать, в зависимости от полученных значений. Настроить некие пороги срабатывания, приводящие к тем или иным последствиям.

Цифровой сигнал, в данном случае, пока, мало меня волнует. Он дает только один порог: 0 или 1. А, вот, аналоговый выход, может позволить мне настроить несколько порогов. Типа, шум ниже значения M = “тишина“, шум выше значения M, но ниже значения N = “средний шум“, а шум выше значения N = “стало слишком шумно, пора валить“.

И теперь вызовем из памяти пример выше, про дом, тишину, макак и комаров. Уже понимаете к чему я веду? Правильно: скажем некий порог в M конкретных единиц ночью – это одно. И тот же самый порог в те же самые M конкретных единиц днем – уже совсем другое. Потому, что, в данном случае, нам, мясным мешкам, приходится оперировать относительными и субъективными понятиями.

У строгой ардуины нет понятий о субъективности и относительности. У нее есть только значение М, назначенное для порога срабатывания. И “громко” это в данный конкретный момент (и надо ли пошевелить ухом, моргнуть светодиодом) или “тихо” – она, на самом деле, понятия не имеет. И получается, что порог срабатывания, настроенный для одной обстановки, становится совершенно неадекватным в другой.

Чтобы научить робота оценивать уместность тех или иных своих реакций в той или иной “звуковой обстановке”, пришлось сперва научить его понимать такую условную, субъективную и относительную вещь, как “общий текущий звуковой фон”.

Для этого, при инициализации (и, потом, каждую минуту), робот берет “пробу окружающего звука”. Он делает подряд по 500 считываний с каждого из своих датчиков. Усредненное значение этой группы показаний запоминается и считается с этого момента условно “нулем” (до следующей сессии автокалибровки под текущий шумовой фон). И вот уже относительно этого условного “нуля”, идет последующая оценка порогов срабатывания. Т.е. пороговое значение для срабатывания не является фиксированной константой. Оно является модульным значением (всегда положительным) относительно текущего “нуля”.

Делать автокалибровку перед проверкой каждого порога при каждом проходе цикла смысла нет. Во-первых, это довольно ресурсоемкая процедура для процессора ардуины и ее ресурсов памяти. Во-вторых, сенсоры опрашиваются в среднем, со скоростью в каждые 5-10 миллисекунд. Маловероятно, что звуковой фон ощутимо изменится в этом интервале времени. Раз в минуту – подобранный опытным путем интервал времени, при котором робот продолжает реагировать на шум более-менее адекватно.


Безусловно, сплошь и рядом будет возникать ситуация, когда между циклами автокалибровки робот будет перемещаться из области одного шумового фона в область с другим шумовым фоном. Ну, скажем, заглянет ко мне в мастерскую с воющим фрезером после относительной тишины комнат дома. Какое-то время он будет себя вести неадекватно и считать, что стало очень громко. Но через минуту-другую он уже “привыкнет”. Как только пройдет очередной цикл автокалибрации. После этого, текущий звуковой фон станет для него “нормальным” и реакции придут в норму. На фоне воющей фрезы он начнет даже выделять музыку, если она орет громче этой самой фрезы и начнет под нее подмигивать, например.

В сущности, с любым читающим этот блог, постоянно происходит в точности то же самое. Зайдите из тихого вечернего переулка в кабак, где громко играет живая музыка и вам сразу “шибанет по ушам”. Потом вы привыкнете и сможете даже разговаривать с соседом по столику. Выйдите обратно в переулок и его тишина вас “оглушит”. Опять же, пока не привыкните.

Механическая собачка теперь реагирует на все это в точности так же. И я решил, что это хорошо.


Свет

С этим – совсем просто. “Темно” или “светло”, конечно, тоже понятия относительные. Но в данном случае, опыты показали, что для реакций робота, это уже не так существенно, как в случае со звуком.

Сейчас робот различает два порога освещенности, которые позволяют ему уверенно выделить три состояния окружающей среды:
“светло”<-[порог1]<-“полумрак”->[порог2]->”темно”.
Пока эти пороги установлены приблизительно, по уровню моего собственного субъективного восприятия текущего уровня освещения.

Сенсоры освещения построены на базе обычных фоторезисторов.

Схему подключения такого фоторезистора к ардуине можно с пол пинка найти в гугле. Например, для совсем простого случая:

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

Здесь и далее везде, при возникновении подобной ситуации, я отказался от идеи изготавливать какую-то дополнительную печатную плату под этот единственный дополнительный резистор. Подобная ситуация для ардуины встречается повсеместно и слишком часто. Каждый раз городить новую плату – слишком трудоемко. Сделать какую-то одну плату под исключительно сборку из миллиона резисторов… Ну, можно было бы, наверное… Кабы я еще знал заранее, сколько там мне таких резисторов надо внутри проекта. Они ж, до кучи, еще и все разными могут быть…

Короче, гораздо проще, такой резистор “встроить” прямо в провод идущий от сенсора. Запаял, термоусадкой обтянул и все:

Просто, быстро, надежно, места почти не занимает и т.п.

В распоряжении у робота есть два фоторезистора по бокам от носа, которые пасут уровень освещения с каждой из сторон головы. Если ему надо получить некое общее представление об уровне освещенности вокруг (как например “общий уровень шума” описанный выше), то просто берется среднее показание с обоих сенсоров. Если роботу важно различать, что там справа, а что слева (как это нужно для светодиодов подсветки) – берется значение сенсора справа или слева соответственно.


Осязание

Организовано по принципу “capacitive sensor”, как уже говорилось ранее. Три датчика: ствол пушки, два уха. Точно так же, как и сенсоры освещения, это очень примитивные устройства, требующие дополнительно только провода, да очередные обыкновенные резисторы. Вряд ли, что еще можно про них сказать подробнее, чем было уже рассказано ранее.

Ну, разве, отметить, что окончательно настроен только сенсор пушки. Чувствительность такого сенсора сильно зависит от “площади контакта” и свойств проводящего материала. В зависимости от этого, чувствительность настраивается путем подбора резистора в районе 1 ома. Плюс-минус туда-сюда.

Так вот… Пушка – она уже есть. И для нее резистор подобран. А ушей пока нет. И хрен его там знает из чего они в итоге будут и насколько хорошо чего куда будут проводить. Пока там временно под них подобраны резисторы, достаточные для реагирования на касания к обычным металлическим шайбам по бокам головы. Будут уши – тогда и подберу все окончательно.


Подавление шумов

Чего уж греха таить. Я не ракету на Марс с живыми людьми собираю. Конечно, стараюсь держать качество исполнения на высоком уровне, когда это возможно. Но, одновременно с этим, я завишу и от качества исполнения приобретаемых электронных компонентов.

Бюджет на проект у меня небольшой. Влить пару-тройку косарей в передовые технологии – не могу себе позволить. Как следствие, зачастую, выбор падает на довольно дешевые “китайские” компоненты ожидаемого качества.

С цифровыми устройствами и сенсорами проще. Оно и понятно: там всегда, либо 0, либо 1 – без вариантов. А вот для аналоговых сигналов характерны всякие “паразитные шумы” и наводки. От проводки, от работающих вокруг устройств, от моих кривых рук, от кривых рук китайцев, от, просто, расположения звезд на небе…

Как следствие, нельзя просто так взять и войти в Мордор считать показание фотосенсора, например. Потому, что никогда не знаешь, вот это самое считанное сейчас – оно правда? Или наводка какая и надо пересчитать еще разок?

К счастью, я не первый человек на планете, кто столкнулся с этой проблемой. Ее очень легко решить при помощи взятия медианы от массива. Не путать с взятием среднего значения массива.

Поясню на примере… Скажем, вы делаете подряд пять считываний с датчика и получаете некий массив:

3, 4, 3, 28, 5

В данном примере, считанное значение 28 – паразит. Шум. Наводка. Ошибка. Вранье. Остальное – правда.

Если вы попробуете взять среднее значение от этого набора, то получите 9 (8.6, на самом деле, но – округлим). Что будет мало походить на правду в общем случае. Правда будет где-то в районе от 3 до 5, что сильно меньше полученных средних 9. И это существенная разница. Поэтому усреднение в данном случае не канает ваще никак.

С другой стороны, если вы сперва отсортируете массив (3, 3, 4, 5, 28) и потом возьмете из него медиану (центральное значение), то получите 4. Что удивительным образом как раз и окажется тем самым “в районе от 3 до 5, которые нам и нужны.

Описанный способ отлично решает вопрос подавления ненужных шумов при опросе аналоговых датчиков. Причем не надо каждый раз, при каждом проходе цикла читать их по пять раз. Это лишнее. Считайте значение только один раз. Просто помните где-нибудь предыдущие четыре раза. Вот вам и медиана по факту последних пяти считываний. И она будет максимально приближенной к реальности правдой с отсечкой значительной части паразитных значений.

Я не знаю… Может оно и есть где в стандартных сишных библиотеках взятие медианы с массива, но не вижу смысла грузить ардуину этими библиотеками. В интернетах полно примеров этой простой функции. Слегка адаптированный общий вариант “из учебника” под мои конкретные задачи:

// Get median value of array: median(array, elemeents)
void median_swap (short *xp, short *yp) {
  unsigned short temp = *xp;
  *xp = *yp;
  *yp = temp;
}
short median (short arr[], short n) {
  unsigned char i, j;
  for (i = 0; i < n - 1; i++)
    for (j = 0; j < n - i - 1; j++)
      if (arr[j] > arr[j + 1])
        median_swap(&arr[j], &arr[j + 1]);
  unsigned char element = n / 2;
  return arr[element];
}

Сортировка массива методом “пузырька” (bubble sort):

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

Безусловно, взамен вы получите некоторую заторможенность в реакции датчика. Но, право же… Датчик опрашивается каждые 2-5 миллисекунд. МИЛЛИСЕКУНД! Ну, тормознет он с правильным ответом на 10-15 миллисекунд… При этом, оно вовсе не означает, что вы в этот промежуток времени данных не получите. Просто, вместо расстояния до стены в один дюйм, вы получите расстояние, которое было за 10 миллисекунд до этого. Какой-нибудь 1.01 дюйма. Которые в итоге, все равно обрежется до 1 целого дюйма переменной типа int, в которой оно и хранится… Короче, возникающей задержкой можно смело пренебречь.


Обмен данными с мозгом

Это хорошо, что в пределах одного контроллера устройства как-то взаимодействуют, рефлексируют и что-то такое друг-другу передают. Но что это все даст будущему мозгу?

Вот полный список данных, который получает мозг от контроллера по запросу:

  • Пушка (одна штука):
    • Текущий режим (вкл/выкл/авто)
    • Текущий угол (градус)
  • Уши (две штуки):
    • Текущий режим (вкл/выкл/авто)
    • Текущий угол (градус)
  • Каждый из сенсоров звука (две штуки):
    • Значение текущего звукового фона (в абстрактных попугаях)
    • Последнее считанное значение
      (в тех же попугаях)
  • Каждый из сенсоров освещения (две штуки):
    • Текущий уровень освещения с той или иной стороны (в абстрактных попугаях)
  • Каждый и сенсоров касания (три штуки):
    • Время, прошедшее с момента последнего касания (mils)
    • Время, прошедшее с момента, когда данный конкретный сенсор задолбали в последний раз (mils)
  • Каждый из светодиодов (четыре штуки):
    • Текущий режим (вкл/выкл/авто)
    • Текущее состояние (горит/не горит/мигает[с какой частотой])
  • Общие данные:
    • Количество свободных позиций в каждой из очередей (четыре штуки: пушка, ухо, ухо, пищалка)

Это то, что мозг может услышать от контроллера. Теперь, то, что мозг может ему приказать… Знаете, тут, думаю, я просто дампну кусок констант, описывающих все доступные команды. Так оно будет проще:

// head_DSArray1 commands
// =========================================================================
//                                            Mode:
//                                            ------------------------------
#define CMD_DSA1_BYTS_MODE 2                  // Number of bytes:   CMD + Mode
#define CMD_DSA1_WPON_MODE 1                  // Weapon:            Set (u c)Mode = [0 - off; 1 - master commands only; 2 - auto]
#define CMD_DSA1_REAR_MODE 2                  // Right ear:         Set (u c)Mode = [0 - off; 1 - master commands only; 2 - auto]
#define CMD_DSA1_LEAR_MODE 3                  // Left ear:          Set (u c)Mode = [0 - off; 1 - master commands only; 2 - auto]
#define CMD_DSA1_AEAR_MODE 123                // All ears:          Set (u c)Mode = [0 - off; 1 - master commands only; 2 - auto]
#define CMD_DSA1_LED1_MODE 4                  // Top right LED:     Set (u c)Mode = [0 - off; 1 - on; 2 - auto; 3-255 - blink frequence]
#define CMD_DSA1_LED2_MODE 5                  // Bottom right LED:  Set (u c)Mode = [0 - off; 1 - on; 2 - auto; 3-255 - blink frequence]
#define CMD_DSA1_LED3_MODE 6                  // Top left LED:      Set (u c)Mode = [0 - off; 1 - on; 2 - auto; 3-255 - blink frequence]
#define CMD_DSA1_LED4_MODE 7                  // Bottom left LED:   Set (u c)Mode = [0 - off; 1 - on; 2 - auto; 3-255 - blink frequence]
#define CMD_DSA1_LEDA_MODE 147                // All LED:           Set (u c)Mode = [0 - off; 1 - on; 2 - auto; 3-255 - blink frequence]
#define CMD_DSA1_BUZZ_MODE 8                  // Buzzer:            Set (u c)Mode = [0 - off; 1 - master commands only; 2 - auto]

//                                            Queue:
//                                            ------------------------------
#define CMD_DSA1_BYTS_QFLASH 1                // Number of bytes:   CMD
#define CMD_DSA1_WPON_QFLASH 11               // Weapon:            Flush queue [w/o args]
#define CMD_DSA1_REAR_QFLASH 12               // Right ear:         Flush queue [w/o args]
#define CMD_DSA1_LEAR_QFLASH 13               // Left ear:          Flush queue [w/o args]
#define CMD_DSA1_BUZZ_QFLASH 14               // Buzzer:            Flush queue [w/o args]

//                                            Drive:
//                                            ------------------------------
#define CMD_DSA1_BYTS_DRV 3                   // Number of bytes:   CMD + Angle/Freq + Speed/Duration
#define CMD_DSA1_WPON_DRV 21                  // Weapon:            Add (u c)Angle = [0-180] and (u c)Speed = [0-255] to queue
#define CMD_DSA1_REAR_DRV 22                  // Right ear:         Add (u c)Angle = [0-180] and (u c)Speed = [0-255] to queue
#define CMD_DSA1_LEAR_DRV 23                  // Left ear:          Add (u c)Angle = [0-180] and (u c)Speed = [0-255] to queue
#define CMD_DSA1_AEAR_DRV 24                  // Both ears:         Add (u c)Angle = [0-180] and (u c)Speed = [0-255] to queue
#define CMD_DSA1_BUZZ_DRV 25                  // Buzzer:            Add (u s)Freq = [tone] and (u s)Duration = [millis] to queue

Настройки контроллера

Ой, блин, в словах я это выразить не решусь. Поэтому, кому охота, могут попробовать понять общие масштабы приключения по вот этому очередному дампу из файла конфигурации:

#define SERIAL_SPEED 115200               // Serial port speed

//                                Weapon servo
#define PIN_WEAPON 9                    // Weapon servo controlPin
#define WEAPON_MIN 25                   // Min angle
#define WEAPON_MAX 147                  // Max angle
#define WEAPON_DEFAULT 110              // Default position angle
#define WEAPON_TOUCH 200                // Sensitivity level for the weapon "unconditioned reflex"
#define WEAPON_CMD_QTYPE FIFO           // Queue type for the servo (FIFO, LIFO)
#define WEAPON_CMD_QSIZE 50             // Queue size for the servo

//                                Right ear servo
#define PIN_EAR_RIGHT 12                // Right ear servo controlPin
#define EAR_RIGHT_MIN 10                // Min angle 
#define EAR_RIGHT_MAX 160               // Max angle
#define EAR_RIGHT_DEFAULT 60            // Default position angle
#define EAR_RIGHT_TOUCH 2               // Sensitivity level for the right ear "unconditioned reflex"
#define EAR_RIGHT_CMD_QTYPE FIFO        // Queue type for the servo (FIFO, LIFO)
#define EAR_RIGHT_CMD_QSIZE 50          // Queue size for the servo

//                                Left ear servo
#define PIN_EAR_LEFT 11                 // Left ear servo controlPin
#define EAR_LEFT_MIN 10                 // Min angle
#define EAR_LEFT_MAX 160                // Max angle
#define EAR_LEFT_DEFAULT 60             // Default position angle
#define EAR_LEFT_TOUCH 2                // Sensitivity level for the left ear "unconditioned reflex"
#define EAR_LEFT_CMD_QTYPE FIFO         // Queue type for the servo (FIFO, LIFO)
#define EAR_LEFT_CMD_QSIZE 50           // Queue size for the servo

//                                LEDs
#define PIN_LED_LEFT1 2                 // Top Left LED controlPin
#define PIN_LED_LEFT2 3                 // Bottom Left LED controlPin
#define PIN_LED_RIGHT1 4                // Top Right LED controlPin
#define PIN_LED_RIGHT2 5                // Bottom Right LED controlPin

//                                Touch sensors
#define PIN_TOUCH_BASE 10               // base byte sendPin
#define PIN_TOUCH_WEAPON 7              // weapon sensor byte receivePin
#define PIN_TOUCH_EAR_RIGHT 8           // Right ear sensor byte receivePin
#define PIN_TOUCH_EAR_LEFT 6            // Left ear sensor byte receivePin
#define TOUCH_SENS 30                   // Byte samples for all the sensors. Can be used to increase the returned resolution, at the expense of slower performance.
#define TOUCH_THRESHOLD 100             // Minimum delay between touches
#define TOUCH_FREQ 3000                 // Touch frequence for row of touches (millis)
#define TOUCH_SND 200                   // Touch default sound freq
#define TOUCH_SND_DURATION 20           // Touch default sound duration
#define TOUCH_DOSTAL_MIN 7              // Min touches before call "dostal" function
#define TOUCH_DOSTAL_MAX 25             // Max touches to call "dostal" function
#define TOUCH_DOSTAL_PROBABILITY 25     // Probability (%) of "dostal" function call, between TOUCH_DOSTAL_MIN and TOUCH_DOSTAL_MAX
#define TOUCH_SND_DOSTAL 100            // Touch "dostal" sound freq
#define TOUCH_SND_DOSTAL_DURATION 2000  // Touch "dostal" sound minimum duration. Will be randomly extended up to TOUCH_SND_DOSTAL_DURATION + TOUCH_SND_DOSTAL_EXTENDER
#define TOUCH_SND_DOSTAL_EXTENDER 5000  // Touch "dostal" sound duration multiplier.
#define TOUCH_RFL_ANGLE 10              // "unconditioned reflex" angle for all the sensors
#define TOUCH_RFL_DELAY 75              // Delay for "unconditioned reflex" action sequences

//                                Buzzer
#define PIN_BUZZER1 13                  // Busser controlPin
#define BUZZER_CMD_QTYPE FIFO           // Queue type for the servo (FIFO, LIFO)
#define BUZZER_CMD_QSIZE 50             // Queue size for the servo

//                                Photosensors
#define PIN_PHS_RIGHT A6                // Right sensor controlPin
#define PIN_PHS_LEFT A7                 // Left sensor controlPin
#define PHS_MEDIAN_SAMPLE 7             // Array size for median calculation
#define PHS_TWILIGHT_LEVEL 950          // Twilight border value
#define PHS_DARK_LEVEL 1020              // Darkness border value

//                                Soundsensors
#define PIN_SND_RIGHT A1                // Right sound sensor controlPin
#define PIN_SND_LEFT A2                 // Left sound sensor controlPin
#define SND_CALIBRATION_DELAY 60000     // Delay between sensor autocalibration cycles
#define SND_NULL_SAMPLE 1000            // Number of readings for autocalibration
#define SND_AVG_SAMPLE 3               // Array size for average value calculation
#define SND_LVL_LOUD 12                 // Sound loud level
#define SND_LVL_MEDIUM 9                // Sound medium level

Список используемых дополнительных библиотек с ссылками на источники:

#include                               // https://www.arduino.cc/en/Reference/Servo
#include                    // https://playground.arduino.cc/Main/CapacitiveSensor/
#include                                // https://www.arduino.cc/en/Reference/Wire
#include                            // https://github.com/SMFSW/Queue
//photosensor - lib is unnecessary                 // https://learn.adafruit.com/photocells/arduino-code

Как видите, совершенно ничего сложного. Покончив с head_DSArray1, можно теперь перемещаться к обвешиванию head_DSArray2. Значит, придется отложить клавиатуру т.к. сперва необходимо обвешать контроллер устройствами. Чем сейчас и занят… И результаты слегка пугают. Оно ж мне вчера чуть палец не отхвалило, сцуко! Тяга в 20 кило на см. у серва – не так смешно, как кажется… :-(


Я знаю, что выпускаю обновления по проекту редко. Поэтому, вот немного спойлеров про то, на что был угроблен предыдущий месяц и о чем пойдет речь в ближайших частях повествования:

  • Внутренний USB-хаб (полностью готов и работает)
  • Принтер – да! (полностью готов и работает)
  • Второй супер-контроллер head_DSArray2, управляющий:
    • Механизмом поворота головы (готов со второй попытки, за исключением одной маленькой детали, которую придется заменить на алюминий т.к. пластиковый эквивалент долго не протянет)
    • Сенсоры температуры (полностью собраны и закодены)
    • Сенсоры влажности воздуха (полностью собраны и закодены)
    • Газовые анализаторы: от СO2 и бытового газа до алкоголя и некоего абстрактного air quality (протестированы и более-менее закодены, необходимо: разработать очередное крепление, подключить по месту и придумать, как их откалибровать)
    • Механизм наклона шеи. Этот – прямо сейчас в стадии активной разработки, печати, сборке (готов наполовину, но еще не подключался и не испытывался):

Такие дела… Продолжение читайте в следующих частях повествования…