Мода на ИИ-помощников, кажется, достигла своего пика. Даже далекие от этой темы люди начинают интересоваться, что это за OpenClaw такой и как бы его установить. Более прошаренные скупают Mac Mini M4 просто потому, что 16 Гб объединенной памяти + нейропроцессорные ядра позволяют гонять локальные модели, а само по себе устройство тихое и с небольшим энергопотреблением.

Если присмотреться получше, у OpenClaw есть не только достоинства, но и ощутимые недостатки. Речь даже не о безопасности, а банально о системных требованиях. В частности, базовый минимум — 2 Гб ОЗУ, а рекомендованное значение — вообще 4 Гб. Плюс CPU нужен хотя бы 4-поточный, иначе возможны проблемы при работе с параллельными задачами.

Вот тут на сцене появляются наши китайские товарищи из Sipeed. Да-да, те самые, которые делают миниатюрный NanoKVM (о нем мы рассказывали в конце 2025 года). «Подержите наше Tsingtao», — заявили они и выпустили PicoClaw — AI-ассистента, которому надо всего лишь 10 Мб ОЗУ и который работает на любом одноядерном CPU со скоростью 600 МГц. Сегодня мы глянем на это чудо китайской мысли внимательно и запустим его на Arduino Uno Q

Содержание:

Что за зверь PicoClaw

Сравнение PicoClaw и OpenClaw (источник изображения)
Сравнение PicoClaw и OpenClaw (источник изображения)

TL;DR

PicoClaw это не OpenClaw, он не способен вытянуть сложные сценарии. При этом для простых задач он идеален и в паре с локальными моделями, и с запущенными где-нибудь на отдельном ПК c GPU (либо напрямую с сервисами вроде ChatGPT, Claude и т. п.). 

PicoClaw стоит рассматривать сразу в контексте экосистемы. Sipeed сконцентрировала бизнес на продаже плат для разработки. Все они строятся вокруг RISC-V-архитектуры и достаточно слабых (но крайне дешевых) процессоров. Например, LicheeRV Nano сделан на SoC SOPHGO SG2002 — внутри пара ядер RISC-V C906, одно из которых работает на частоте 1 ГГц, а второе — на 700 МГц. ОЗУ тоже мало: всего 256 Мб.

Все это накладывает серьезное ограничение на софт. Тут уже не будет места для массивного рантайма вроде Node 24, который использует OpenClaw. Вместо этого «младший брат» почти целиком написан на Go. Именно это обстоятельство кардинально снижает потребление оперативной памяти. Любопытно то, что код проекта на 95% сгенерирован AI, некоторые platform-specific-штуки подкрутили вручную.

Например, в Makefile можно найти вот такое:

# Patch MIPS LE ELF e_flags (offset 36) for NaN2008-only kernels
# (e.g. Ingenic X2600).
# Bytes (octal): \004 \024 \000 \160 → little-endian 0x70001404
#   0x70000000  EF_MIPS_ARCH_32R2   MIPS32 Release 2

Go для MIPS собирает softfloat-бинарники и не ставит флаг EF_MIPS_NAN2008. Ядро Ingenic X2600 работает в NaN2008-only-режиме и отказывается грузить такой ELF, хотя FP-инструкций в бинаре нет вообще. Патч правит 4 байта по смещению 36 в e_flags — чисто косметика для ядра.

Отдельной экстремальной оптимизации тут нет — она досталась бонусом с выбором рантайма. Дополнительно в этом роль играет грамотный релиз-инжиниринг, вроде build tags для тяжелых зависимостей и флагов линковщика, отрубающих таблицу символов и DWARF. Экономия составила почти треть размера исполняемого файла. 

Забавно, что сборщик мусора работает на дефолтных настройках — никаких нестандартных GC-политик. PicoClaw создавался скорее не как конкурент OpenClaw, а как киллер-фича: вы можете запустить собственного AI-ассистента на плате за 10 долларов. К слову, список поддерживаемых архитектур впечатляет. Помимо стандартной тройки Windows/Linux/macOS, тут есть FreeBSD, NetBSD и даже Android. 

Кажется, мне стоит попробовать запустить его на Nintendo Wii. На Хабре мы рассказывали, как установить туда современную NetBSD. Кстати, напишите в комментариях, интересно ли вам такое. А пока что расчехлю одноплатник Arduino Uno Q, который с недавнего времени обзавелся модным корпусом, напечатанным на 3D-принтере.

Установка и первый запуск

Uno Q функционирует под управлением операционной системы Debian, поэтому к нему применимы все те же самые инструкции, как и к любому другому компьютеру на Linux. На момент подготовки этой статьи актуальной была версия 0.2.1, поэтому скачиваем соответствующий пакет с GitHub:

$ wget https://github.com/sipeed/picoclaw/releases/download/v0.2.1/picoclaw_aarch64.deb

Устанавливаем при помощи пакетного менеджера dpkg:

$ sudo dpkg -i picoclaw_aarch64.deb

Теперь выполняем команду, которая базово конфигурирует AI-ассистента, аналог openclaw onboard:

$ picoclaw onboard

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

$ nano /home/arduino/.picoclaw/config.json

Я решил использовать локальную модель на своем домашнем ПК. qwen3-4b-thinking-2507 можно поставить буквально в один клик из LM Studio, и она неплохо справляется. Важно при этом не забыть запустить локальный сервер, а в файле конфигурации PicoClaw указать модель в двух местах.

Секция agents: 
"agents": {

    "defaults": {

      "workspace": "/home/arduino/.picoclaw/workspace",

      "restrict_to_workspace": true,

      "allow_read_outside_workspace": false,

      "provider": "qwen",

      "model": "qwen3-4b-thinking-2507",

      "max_tokens": 32768,

      "max_tool_iterations": 50,

      "summarize_message_threshold": 20,

      "summarize_token_percent": 75

    }

Секция model_list:

"model_list": [

    {

      "model_name": "qwen3-4b-thinking-2507",

      "model": "qwen/qwen3-4b-thinking-2507",

      "api_base": "http://192.168.88.6:1234/v1",

      "api_key": ""

    }

],

Теперь можно запустить приложение командой:

$ picoclaw gateway

Отображение статуса

Работающий AI-ассистент — это прекрасно. Но тут мы вспоминаем, что, вообще-то, у нас не просто одноплатник, а Uno Q, у которого, помимо MPU, есть MCU STM32U585 — можно запускать скетчи для Arduino. Более того, в девайс встроена LED-матрица аж на 104 светодиода. Понятно, что этого очень мало: даже у первых тамагочи экранчик был поинтереснее (32 × 16). 

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

Где брать статус

Пожалуй, это был самый первый вопрос, на который предстояло ответить. Внимательно изучив логи и репозиторий проекта, пришел к неутешительному выводу: фактически у PicoClaw нет штатного API для внешнего наблюдения за его рабочим состоянием. Есть лишь пара эндпойнтов:

  • /health — бинарный жив/мертв;

  • /ready — готов / не готов.

Мне же хотелось отображать, когда ассистент думает, принимает/отправляет сообщения и когда он свободен. Увы, но этих четырех состояний у PicoClaw просто нет. Конечно, в теории можно было бы форкнуть, залезть в код и уже там сделать отдельный эндпойнт, например /status. Но это путь самурая: сложно и не факт, что получится быстро.

Мое внимание привлек лог в stdout. Дело в том, что именно туда ассистент пишет вполне читабельные фразы вида Processing message from, LLM response / Routed message и Published outbound response, которые как раз подходят для определения состояния. Оставалось лишь перенаправить stdout в файл, а дальше написать крошечный http-сервер, реализуя отсутствующий штатный эндпойнт:

тык
import http.server, json, re, time

LOG = "/home/arduino/picoclaw.log"

RE_IN  = re.compile(r"Processing message from")

RE_LLM = re.compile(r"LLM response|Routed message")

RE_OUT = re.compile(r"Published outbound response")

_state = "ready"

statetime = time.time()

IDLE_AFTER = 4.0

_pos = 0

def seek_end():

    global _pos

    try:

        with open(LOG) as f:

            f.seek(0, 2)

            _pos = f.tell()

    except:

        _pos = 0

def update_state():

    global state, state_time, _pos

    try:

        with open(LOG) as f:

            f.seek(_pos)

            lines = f.read().splitlines()

            _pos = f.tell()

        for line in lines:

            if RE_IN.search(line):

                state = "receiving"; state_time = time.time()

            elif RE_LLM.search(line):

                state = "thinking"; state_time = time.time()

            elif RE_OUT.search(line):

                state = "sending"; state_time = time.time()

    except:

        pass

    if state != "ready" and time.time() - state_time > IDLE_AFTER:

        _state = "ready"

class H(http.server.BaseHTTPRequestHandler):

    def do_GET(self):

        update_state()

        body = json.dumps({"state": _state}).encode()

        self.send_response(200)

        self.send_header("Content-Type", "application/json")

        self.end_headers()

        self.wfile.write(body)

    def log_message(self, *a):

        pass

seek_end()

print("State server :18791")

http.server.HTTPServer(("0.0.0.0", 18791), H).serve_forever()

Тут отмечу только один момент: статус порой «залипает», поэтому добавил сброс после 4 секунд (выяснил эмпирическим путем). Пришла пора разобраться, как сделать анимации.

Встроенная LED-матрица

При горизонтальной ориентации платы у нас получается 8 строк и 13 столбцов (формат: uint32_t[4], 104 бита, построчно слева-направо, сверху-вниз). Для работы с ней нужно включить библиотеку Arduino_LED_Matrix в скетч и далее инициализировать ее примерно так:

#include "Arduino_LED_Matrix.h"

ArduinoLEDMatrix matrix;

void setup() {

 Serial.begin(115200);

 matrix.begin();

}

Теперь пришла пора продумать сами иконки и анимации. Всего их будет пять:

  1. Thinking — «матрица» (процедурные пиксели бегут вниз).

  2. Receiving — антенна со сходящимися волнами.

  3. Sending — антенна с расходящимися волнами.

  4. Ready — статичное изображение краба.

  5. Clear — пустая «матрица».

Сначала для статуса Thinking я пробовал сделать что-то вроде шестеренки, но при столь малом разрешении это выглядит вообще невнятно. Потом допилил «матрицу», и получилось довольно зрелищно. 

тык
void packAndDisplay(const uint16_t* rows) {

  uint32_t f[4] = {0, 0, 0, 0};

  for (int row = 0; row < 8; row++) {

    for (int col = 0; col < 13; col++) {

      if (rows[row] & (1 << (12 - col))) {

        int globalBit = row * 13 + col;

        int word = globalBit / 32;

        int pos  = 31 - (globalBit % 32);

        f[word] |= (1UL << pos);

      }

    }

  }

  matrix.loadFrame(f);

}

void clearMatrix() {

  uint32_t f[4] = {0, 0, 0, 0};

  matrix.loadFrame(f);

}

int8_t  dropY[13];

int8_t  dropLen[13];

uint8_t dropSpeed[13];

uint8_t dropTick[13];

void matrixInit() {

  for (int c = 0; c < 13; c++) {

    dropY[c]     = -(random(0, 10));

    dropLen[c]   = random(2, 6);

    dropSpeed[c] = random(1, 4);

    dropTick[c]  = 0;

  }

}

void matrixStep() {

  uint16_t rows[8] = {0};

  for (int c = 0; c < 13; c++) {

    dropTick[c]++;

    if (dropTick[c] >= dropSpeed[c]) {

      dropTick[c] = 0;

      dropY[c]++;

      if (dropY[c] - dropLen[c] > 7) {

        dropY[c]     = -(random(0, 8));

        dropLen[c]   = random(2, 6);

        dropSpeed[c] = random(1, 4);

      }

    }

    for (int i = 0; i < dropLen[c]; i++) {

      int y = dropY[c] - i;

      if (y >= 0 && y < 8) {

        rows[y] |= (1 << (12 - c));

      }

    }

  }

  packAndDisplay(rows);

}

Статичная иконка краба рисуется следующим образом:

const uint16_t crab[8] = {

  0b0100000000010,

  0b0010000000100,

  0b0011011101100,

  0b0001111111000,

  0b0011101011100,

  0b0011111111100,

  0b0010101010100,

  0b0010000000100,

};

Результат выглядит как-то так:

Любые анимации — просто отрисованные подобным образом кадры, отправляемые поочередно. Это, кстати, позволило сэкономить код для анимации антенны: сходящиеся и расходящиеся волны можно было реализовать последовательностью вывода кадров (ant0-5):

// Sending: волны ОТ антенны

const uint16_t* ant_send_frames[] = { ant0, ant1, ant2, ant3, ant4, ant5 };

// Receiving: волны К антенне

const uint16_t* ant_recv_frames[] = { ant5, ant4, ant3, ant2, ant1, ant0 };

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

MPU и MCU

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

Слой 1. QRB2210 и STM32U585 соединены друг с другом напрямую через самый обычный UART. Со стороны MCU это порт Serial1, а со стороны Linux — символьное устройство вида /dev/ttyS0. По факту обычный прямой полнодуплексный канал.

Слой 2. Поверх сырых байтов UART живет RPC-протокол на базе MsgPack (бинарная альтернатива JSON). При компиляции скетчей вы часто будете видеть автоматически подтягиваемые библиотеки MsgPack, ArxContainer и ArxTypeTraits — это вот как раз для сериализации RPC-запросов/ответов:

Слой 3. Самая мудреная и нетривиальная часть — маршрутизатор (arduino-router). Дело в том, что скетчи для Uno Q состоят из двух частей: кода на Python и кода скетча. Первый исполняется на MPU, а второй — на MCU. Прикол в том, что прямой связи между ними нет. Вместо этого на Linux-стороне крутится демон arduino-router, который все время держит открытым /dev/ttyS0 и читает/пишет в MCU. Это такой брокер сообщений, связывающий две части воедино и превращий запросы на локальный сокет в запросы к MCU по UART, и наоборот.

Слой 4. Непосредственно клиенты. Со стороны микроконтроллера — библиотека Arduino_RouterBridge, а со стороны Linux — arduino.app_utils, входящая в состав пакета app-bricks-py. Последний, в свою очередь, поставляется вместе с контейнером App Lab.

Зачем там целый роутер, спросите вы? UART таки один, а экземпляров кода на Python в архитектуре Uno Q может быть несколько. Так что arduino-router играет роль мультиплексора, позволяя каждому экземпляру спокойно делить UART без конфликтов. Это же дает сверхспособность переживать рестарты контейнеров, особенно с кодом на Python. Иначе каждый перезапуск приводил бы к необходимости повторного хендшейка с MCU.

Ну и совсем приятный бонус — отсутствие привязки к конкретному ЯП. Если вместо Python вы захотите написать код на чем-то другом — без проблем. Главное — реализовать MsgPack-клиент к роутеру. Скетчу на MCU все эти высокоуровневые штуки по барабану: он общается с любой сущностью, лишь бы последняя умела принимать RPC.

Контейнеры и сеть

Еще одна проблема, которую нужно будет решить, — связность. Дело в том, что App Lab деплоит приложения внутри Docker-контейнера, а мы поставили picoclaw и запустили сервер состояний непосредственно на Linux-хосте. Проблема в том, что между деплоями App Lab может менять имя и шлюз. Если захардкодить конкретный IP, то при любом обновлении образа самого App Lab адрес поменяется и придется лезть в код.

Воспользуемся тем, что внутри контейнера адрес шлюза по умолчанию из /proc/net/route будет IP-адресом хоста со стороны контейнера. Напишем небольшой скрипт автоопределения IP:

тык

def detect_host_ip():

    override = os.environ.get("PICOCLAW_HOST")

    if override:

        print("[net] PICOCLAW_HOST override: {}".format(override), flush=True)

        return override

    try:

        with open("/proc/net/route") as f:

            for line in f.readlines()[1:]:

                fields = line.strip().split()

                # destination == 00000000 → default route

                if len(fields) >= 3 and fields[1] == "00000000":

                    gw_hex = fields[2]

                    ip = ".".join(

                        str(int(gw_hex[i:i+2], 16))

                        for i in range(6, -1, -2)

                    )

                    print("[net] default gateway detected: {}".format(ip), flush=True)

                    return ip

    except Exception as e:

        print("[net][ERROR] detect_host_ip: {}: {}".format(

            type(e).__name__, e), flush=True)

    # Запасной вариант - дефолтный docker bridge

    print("[net] fallback to 172.17.0.1", flush=True)

    return "172.17.0.1"

HOST            = detect_host_ip()

HEALTH_URL      = "http://{}:18790/health".format(HOST)

STATE_URL       = "http://{}:18791/".format(HOST)

POLL_INTERVAL   = 0.5

SENDING_TIMEOUT = 3.0

Теперь не страшно, если App Lab поменяет адрес контейнера, — код всегда подскажет актуальный.

Собираем воедино

Когда все ключевые моменты ясны, пришла пора написать код и вспомогательный скрипт для запуска сервера состояния. Финальный пайплайн выглядит так:

Для ручного старта был создан простой скрипт (start_picoclaw.sh), который, помимо запуска, умеет еще и останавливать работу, если вызвать его с параметром stop:

тык

#!/bin/bash

stop_all() {

    echo "Останавливаем..."

    pkill -f "picoclaw gateway" 2>/dev/null

    pkill -f state_server.py 2>/dev/null

    pkill -f "tee /home/arduino/picoclaw.log" 2>/dev/null

    echo "Готово"

}

start_all() {

    stop_all

    sleep 1

    picoclaw gateway 2>&1 | tee /home/arduino/picoclaw.log &

    sleep 3

    python3 /home/arduino/state_server.py &

    echo "Всё запущено"

}

case "$1" in

    stop)  stop_all ;;

    *)     start_all ;;

esac

Запуск скрипта при перезагрузке я просто запихнул в cron. Теперь достаточно подать питание, и спустя 10 секунд сначала поднимется сам PicoClaw, а затем и сервер состояний:

@reboot sleep 10 && /home/arduino/start_picoclaw.sh

Под спойлерами вы найдете полный код для App Lab. Всего три файла:

тык
main.py

from arduino.app_utils import *

import time

import os

import traceback

try:

    import urllib.request

    import json

    HAS_HTTP = True

except ImportError:

    HAS_HTTP = False

    print("[WARNING] urllib недоступен", flush=True)

def detect_host_ip():

    override = os.environ.get("PICOCLAW_HOST")

    if override:

        print("[net] PICOCLAW_HOST override: {}".format(override), flush=True)

        return override

    try:

        with open("/proc/net/route") as f:

            for line in f.readlines()[1:]:

                fields = line.strip().split()

                if len(fields) >= 3 and fields[1] == "00000000":

                    gw_hex = fields[2]

                    ip = ".".join(

                        str(int(gw_hex[i:i+2], 16))

                        for i in range(6, -1, -2)

                    )

                    print("[net] default gateway detected: {}".format(ip), flush=True)

                    return ip

    except Exception as e:

        print("[net][ERROR] detect_host_ip: {}: {}".format(

            type(e).__name__, e), flush=True)

    print("[net] fallback to 172.17.0.1", flush=True)

    return "172.17.0.1"

HOST = detect_host_ip()

HEALTH_URL = "http://{}:18790/health".format(HOST)

STATE_URL = "http://{}:18791/".format(HOST)

POLL_INTERVAL = 0.5

SENDING_TIMEOUT = 3.0

currentanim = None

def set_led(name, force=False):

    global currentanim

    if currentanim == name and not force:

        return

    currentanim = name

    try:

        result = Bridge.call("set_animation", name)

        print("[LED] -> {} (bridge: {!r})".format(name, result), flush=True)

    except Exception as e:

        print("[LED][ERROR] set_animation({}): {}: {}".format(

            name, type(e).__name__, e), flush=True)

        traceback.print_exc()

httperr_logged = False

def http_get(url):

    global httperr_logged

    try:

        resp = urllib.request.urlopen(url, timeout=1)

        return json.loads(resp.read().decode())

    except Exception as e:

        if not httperr_logged:

            print("[HTTP][ERROR] {} -> {}: {}".format(

                url, type(e).__name__, e), flush=True)

            httperr_logged = True

        return None

def http_ok_again():

    global httperr_logged

    if httperr_logged:

        print("[HTTP] связь восстановлена", flush=True)

        httperr_logged = False

_started = False

picoclawalive = False

misscount = 0

MISS_MAX = 4

lastsending_t = 0

prevstate = ""

def loop():

    global picoclawalive, misscount, lastsending_t, prevstate, _started

    if not _started:

        _started = True

        print("[start] PicoClaw LED monitor", flush=True)

        print("[start] HEALTH_URL = {}".format(HEALTH_URL), flush=True)

        print("[start] STATE_URL  = {}".format(STATE_URL), flush=True)

        set_led("thinking", force=True)

    health = http_get(HEALTH_URL)

    if health is None:

        misscount += 1

        if misscount == MISS_MAX:

            print("[poll] picoclaw не отвечает", flush=True)

        if misscount >= MISS_MAX and picoclawalive:

            picoclawalive = False

            set_led("clear")

        time.sleep(POLL_INTERVAL)

        return

    http_ok_again()

    misscount = 0

    if not picoclawalive:

        print("[poll] picoclaw появился", flush=True)

        picoclawalive = True

    state_data = http_get(STATE_URL)

    if state_data is None:

        set_led("ready")

        time.sleep(POLL_INTERVAL)

        return

    state = state_data.get("state", "ready")

    if state == "sending":

        if prevstate != "sending":

            lastsending_t = time.time()

        set_led("sending")

    elif state == "thinking":

        set_led("thinking")

    elif state == "receiving":

        set_led("receiving")

    else:

        if currentanim == "sending":

            if time.time() - lastsending_t < SENDING_TIMEOUT:

                pass

            else:

                set_led("ready")

        else:

            set_led("ready")

    prevstate = state

    time.sleep(POLL_INTERVAL)

App.run(user_loop=loop)

sketch.ino

#include "Arduino_LED_Matrix.h"

#include "Arduino_RouterBridge.h"

ArduinoLEDMatrix matrix;

enum State {

  STATE_CLEAR,

  STATE_THINKING,

  STATE_RECEIVING,

  STATE_SENDING,

  STATE_READY

};

volatile State currentState = STATE_CLEAR;

volatile bool  stateChanged = false;

unsigned long lastFrameTime = 0;

int  frameIndex             = 0;

bool animationDone          = false;

const unsigned long MATRIX_SPEED   = 80;

const unsigned long ANTENNA_SPEED   = 200;

const unsigned long CRAB_SPEED     = 500;

void packAndDisplay(const uint16_t* rows) {

  uint32_t f[4] = {0, 0, 0, 0};

  for (int row = 0; row < 8; row++) {

    for (int col = 0; col < 13; col++) {

      if (rows[row] & (1 << (12 - col))) {

        int globalBit = row * 13 + col;

        int word = globalBit / 32;

        int pos  = 31 - (globalBit % 32);

        f[word] |= (1UL << pos);

      }

    }

  }

  matrix.loadFrame(f);

}

void clearMatrix() {

  uint32_t f[4] = {0, 0, 0, 0};

  matrix.loadFrame(f);

}

int8_t  dropY[13];      

int8_t  dropLen[13];    

uint8_t dropSpeed[13];  

uint8_t dropTick[13];    

void matrixInit() {

  for (int c = 0; c < 13; c++) {

    dropY[c]     = -(random(0, 10));  

    dropLen[c]   = random(2, 6);

    dropSpeed[c] = random(1, 4);

    dropTick[c]  = 0;

  }

}

void matrixStep() {

  uint16_t rows[8] = {0};

  for (int c = 0; c < 13; c++) {

    dropTick[c]++;

    if (dropTick[c] >= dropSpeed[c]) {

      dropTick[c] = 0;

      dropY[c]++;

     

      if (dropY[c] - dropLen[c] > 7) {

        dropY[c]     = -(random(0, 8));

        dropLen[c]   = random(2, 6);

        dropSpeed[c] = random(1, 4);

      }

    }

   

    for (int i = 0; i < dropLen[c]; i++) {

      int y = dropY[c] - i;

      if (y >= 0 && y < 8) {

        rows[y] |= (1 << (12 - c));

      }

    }

  }

  packAndDisplay(rows);

}

const uint16_t ant0[8] = {

  0b0000000000000,  

  0b0000000000000,  

  0b0000000000000,  

  0b0000000000000,  

  0b0000000000000,  

  0b0000001000000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t ant1[8] = {

  0b0000000000000,  

  0b0000000000000,  

  0b0000000000000,  

  0b0000010100000,  

  0b0000001000000,  

  0b0000001000000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t ant2[8] = {

  0b0000000000000,  

  0b0000100010000,  

  0b0000010100000,  

  0b0000000000000,  

  0b0000010100000,  

  0b0000001000000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t ant3[8] = {

  0b0001000001000,  

  0b0000100010000,  

  0b0000000000000,  

  0b0000100010000,  

  0b0000010100000,  

  0b0000001000000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t ant4[8] = {

  0b0010000000100,  

  0b0001000001000,  

  0b0000000000000,  

  0b0001000001000,  

  0b0000100010000,  

  0b0000010100000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t ant5[8] = {

  0b0100000000010,  

  0b0010000000100,  

  0b0000000000000,  

  0b0010000000100,  

  0b0001000001000,  

  0b0000100010000,  

  0b0000001000000,  

  0b0000011100000,  

};

const uint16_t* ant_send_frames[] = { ant0, ant1, ant2, ant3, ant4, ant5 };

const uint16_t* ant_recv_frames[] = { ant5, ant4, ant3, ant2, ant1, ant0 };

const int ANT_COUNT = 6;

const uint16_t crab[8] = {

  0b0100000000010,  

  0b0010000000100,  

  0b0011011101100,  

  0b0001111111000,  

  0b0011101011100,  

  0b0011111111100,  

  0b0010101010100,  

  0b0010000000100,  

};

String set_animation(String anim) {

  State newState = STATE_CLEAR;

  if      (anim == "thinking")  newState = STATE_THINKING;

  else if (anim == "receiving") newState = STATE_RECEIVING;

  else if (anim == "sending")   newState = STATE_SENDING;

  else if (anim == "ready")     newState = STATE_READY;

  else if (anim == "clear")     newState = STATE_CLEAR;

  else return String("unknown");

  currentState = newState;

  stateChanged = true;

  return anim;

}

void setup() {

  matrix.begin();

  clearMatrix();

  randomSeed(analogRead(A0));

  Bridge.begin();

  Monitor.begin();

  if (!Bridge.provide("set_animation", set_animation)) {

    Monitor.println("Error: failed to provide set_animation");

  } else {

    Monitor.println("Registered: set_animation");

  }

}

void loop() {

  if (stateChanged) {

    stateChanged  = false;

    frameIndex    = 0;

    animationDone = false;

    lastFrameTime = millis();

    if (currentState == STATE_CLEAR) {

      clearMatrix();

    }

    if (currentState == STATE_THINKING) {

      matrixInit();

    }

    if (currentState == STATE_READY) {

      packAndDisplay(crab);

    }

  }

  if (currentState == STATE_CLEAR) {

    delay(10);

    return;

  }

 

  if (currentState == STATE_READY) {

    delay(50);

    return;

  }

  unsigned long speed;

  switch (currentState) {

    case STATE_THINKING:  speed = MATRIX_SPEED;   break;

    case STATE_RECEIVING: speed = ANTENNA_SPEED; break;

    case STATE_SENDING:   speed = ANTENNA_SPEED; break;

    default: return;

  }

  if (millis() - lastFrameTime < speed) return;

  lastFrameTime = millis();

  switch (currentState) {

    case STATE_THINKING:

      matrixStep();

      break;

    case STATE_RECEIVING:

      if (frameIndex >= ANT_COUNT) { animationDone = true; frameIndex = ANT_COUNT - 1; }

      packAndDisplay(ant_recv_frames[frameIndex]);

      break;

    case STATE_SENDING:

      if (frameIndex >= ANT_COUNT) { animationDone = true; frameIndex = ANT_COUNT - 1; }

      packAndDisplay(ant_send_frames[frameIndex]);

      break;

    default: break;

  }

  if (!animationDone) frameIndex++;

}

sketch.yaml

profiles:

  default:

    fqbn: 

    platforms:

      - platform: arduino:zephyr

    libraries:

      - ArxContainer (0.7.0)

      - ArxTypeTraits (0.3.2)

      - DebugLog (0.8.4)

      - MsgPack (0.4.2)

      - Arduino_RPClite (0.2.1)

      - Arduino_RouterBridge (0.4.0)

      - ArduinoGraphics (1.1.5)

default_profile: default

Заключение

Вот такое получилось интересное погружение в особенности AI-ассистента и платы Arduino Uno Q. Я очень доволен результатом — ведь теперь у меня появилась легковесная альтернатива OpenClaw для простых задач. Вы пишете своему PicoClaw в Telegram, а он стучится в локальную модель или облако, выбирает лучший путь решения и в конце выдает вам ответ тоже в мессенджер.

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

А вы уже пробовали PicoClaw? Ждем вас в комментариях.

Комментарии (0)