Разработка приложений для 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:
- Запустите локальный Gopher-сервер:
go run server.go. - Скомпилируйте клиент под эмулятор:
pdgo build .. - Откройте полученный бандл
gogopher.pdxв Playdate Simulator.
Нажмите кнопку B для инициализации сетевого стека, после чего используйте колесо мыши или клавиши стрелок для имитации вращения кранка. Нажмите A для перехода к выбранному тексту.
Итоги и рекомендации
Разработка под узкоспециализированные устройства на Go благодаря TinyGo и pdgo демонстрирует, что компактный код без лишней абстракции может эффективно работать даже на встроенных микроконтроллерах. Протокол Gopher исключает накладные расходы на парсинг DOM-дерева и рендеринг тяжелой графики, позволяя мгновенно загружать и читать статьи на портативной консоли.
Исходный код готового клиента и примеров доступен в открытом репозитории github.com/akovardin/gogopher.

