Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Иллюстрация к статье

Разработка приложений для Playdate на Go по протоколу Gopher

Пошаговое техническое руководство по разработке приложений для портативной консоли Playdate на языке Go. Инженеры разбирают настройку тулчейна pdgo и пропатченного LLVM, особенности компиляции через TinyGo, а также практическую реализацию серверной и клиентской логики ретро-протокола Gopher.

Разработка приложений для Playdate на Go по протоколу Gopher

Портативная игровая консоль Playdate, разработанная компанией Panic в содружестве с дизайн-бюро Teenage Engineering, привлекает внимание своей концептуальной минималистичностью: черно-белый 2.7-дюймовый экран с высоким разрешением без подсветки, крестовина с двумя кнопками и механическая рукоятка (кранк). Хотя официальный SDK ориентирован на C и Lua, язык Go в сочетании с проектом TinyGo предоставляет возможность писать компактный сетевой код под микроконтроллеры без стандартного тяжёлого рантайма Go.

В качестве прикладного протокола для передачи данных идеально подходит Gopher — ретро-протокол распространения текстовой информации, созданный в 1991 году до массового распространения HTTP. В Gopher нет HTML-разметки, стилей CSS или JavaScript: ресурсы структурируются в виде иерархических текстовых меню и документов. Связка из консоли Playdate, языка Go и протокола Gopher позволяет создать быструю и полностью автономную систему чтения текстового контента.

В данном гайде мы разберем устройство тулчейна pdgo, структуру протокола Gopher, серверную часть на Go и реализацию графического клиента для консоли Playdate.


Архитектура платформы и инструментарий pdgo

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

Среда разработки pdgo связывает C-SDK от Panic, подсистему TinyGo и компилятор LLVM в единый инструмент. Установка выполняется скриптом, который компилирует пропатченную версию LLVM под целевую архитектуру:

# Установка окружения pdgo и подготовка компилятора
curl -sSL https://raw.githubusercontent.com/akovardin/pdgo/main/install.sh | bash

Процесс сборки инструментария занимает от 10 до 20 минут и выполнятся один раз. В результате в системе появляются утилиты pdgo и настроенные переменные окружения. В отличие от C-проектов под Playdate, требующих написания файлов конфигурации pdx.info и Makefile, проект на pdgo описывается минималистичным Taskfile.yml:

version: '3'

tasks:
  build:
    cmds:
      - pdgo build .
  deploy:
    cmds:
      - pdgo build --deploy .

Флаг --deploy при подключении физической консоли по USB автоматически компилирует .pdx бандл и загружает его на устройство.


Сетевой протокол Gopher: серверная логика на Go

Протокол Gopher работает поверх классического TCP-соединения на порту 70. Модель взаимодействия строго последовательна («запрос — ответ»): клиент подключается к серверу, передает строку запроса (селектор), сервер вычитывает запрос, отдаёт порцию текстовых данных и немедленно закрывает TCP-сокет.

Элемент меню Gopher описывается отдельной строкой, где первый символ определяет тип ресурса:

  • i — информационная строка (текстовый пояснительный фрагмент, не являющийся ссылкой);
  • 0 — ссылка на текстовый файл;
  • 1 — ссылка на вложенный каталог или новое меню.

Ниже приведена реализация лёгкого TCP-сервера Gopher на Go:

package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

type Item struct {
    Type     string
    Text     string
    Selector string
    Host     string
    Port     string
}

func main() {
    listener, err := net.Listen("tcp", ":70")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()

    reader := bufio.NewReader(conn)
    selector, err := reader.ReadString('\n')
    if err != nil {
        return
    }
    selector = strings.TrimSpace(selector)

    if selector == "" || selector == "/" {
        sendMenu(conn)
    } else if selector == "/article" {
        sendFile(conn, "Текст новости сайта Код и Капуста...")
    }
}

func sendMenu(conn net.Conn) {
    items := []Item{
        {Type: "i", Text: "Добро пожаловать в Gopher-ридер!", Host: "fake", Port: "70"},
        {Type: "1", Text: "Главные новости недели", Selector: "/news", Host: "gopher.local", Port: "70"},
        {Type: "0", Text: "Читать статью о Playdate", Selector: "/article", Host: "gopher.local", Port: "70"},
    }

    for _, item := range items {
        fmt.Fprintf(conn, "%s%s\t%s\t%s\t%s\r\n", item.Type, item.Text, item.Selector, item.Host, item.Port)
    }
    fmt.Fprintf(conn, ".\r\n") // Точка на отдельной строке обозначает конец ответа
}

func sendFile(conn net.Conn, content string) {
    fmt.Fprintf(conn, "%s\r\n.\r\n", content)
}

Создание клиента под Playdate на Go

Клиентская часть под Playdate должна выполнять три ключевые задачи: считывание физического ввода (кранк и кнопки A/B), работу с сетевым сокетом и растрирование шрифта на монохромном экране.

Playdate не поддерживает векторные шрифты TrueType/OpenType. Для отображения текста используется битовый шрифт — изображение с буквами и служебный файл координат. Конвертировать стандартный шрифт в формат Playdate можно через встроенные утилиты SDK.

1. Подключение к сети и отправка запроса

Фреймворк pdgo предоставляет обёртку network.tcp для установки сетевых соединений. Ответ серверов Gopher обрабатывается через callback-функцию:

package main

import (
    "strings"
    "github.com/akovardin/pdgo/network"
    "github.com/akovardin/pdgo/playdate"
)

var pd *playdate.Playdate

type Client struct {
    menuItems []Item
    view      *View
}

func (c *Client) FetchDirectory(selector string) {
    network.TCPConnect("gopher.local", 70, func(conn *network.TCPConn, err error) {
        if err != nil {
            return
        }
        defer conn.Close()

        conn.WriteString(selector + "\r\n")
        data := conn.ReadAll()
        c.menuItems = parseGopherMenu(string(data))
    })
}
2. Отрисовка графического интерфейса и скроллинг кранком

Каждый кадр система вызывает функцию update(). Мы считываем градус поворота механики кранка, пересчитываем смещение по оси Y и рендерим строки с помощью функции drawText:

func (c *Client) Update() {
    pd.Graphics.Clear(playdate.ColorWhite)

    // Считываем угол поворота механического кранка (в градусах)
    crankChange := pd.System.GetCrankChange()
    c.view.OffsetY += int(crankChange)

    // Отрисовка пунктов меню
    cursorIndex := 0
    for i, item := range c.menuItems {
        yPos := 20 + (i * 18) + c.view.OffsetY
        
        // Фиксированная позиция курсора выбора
        if yPos >= 100 && yPos <= 118 {
            cursorIndex = i
            pd.Graphics.DrawText(">", 5, 100)
        }
        pd.Graphics.DrawText(item.Text, 20, yPos)
    }

    // Обработка кнопки A (выбор текущего элемента)
    if pd.System.ButtonJustPressed(playdate.ButtonA) {
        selected := c.menuItems[cursorIndex]
        if selected.Type == "1" {
            c.FetchDirectory(selected.Selector)
        }
    }
}

Тестирование и отладка

Для локального тестирования без физического устройства запустите эмулятор Playdate Simulator из состава официального Playdate SDK:

  1. Запустите локальный Gopher-сервер: go run server.go.
  2. Скомпилируйте клиент под эмулятор: pdgo build ..
  3. Откройте полученный бандл gogopher.pdx в Playdate Simulator.

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

Итоги и рекомендации

Разработка под узкоспециализированные устройства на Go благодаря TinyGo и pdgo демонстрирует, что компактный код без лишней абстракции может эффективно работать даже на встроенных микроконтроллерах. Протокол Gopher исключает накладные расходы на парсинг DOM-дерева и рендеринг тяжелой графики, позволяя мгновенно загружать и читать статьи на портативной консоли.

Исходный код готового клиента и примеров доступен в открытом репозитории github.com/akovardin/gogopher.