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

image

Вспомнилась статья на Хабре Электронная книжка в качестве дисплея, решено было достать с полки старенький полуживой ридер Sony PRS-505 и дать ему вторую жизнь в роли экрана для контроллера автоматизации. Но слать картинки через флеш память плохая идея. Нужно было научиться работать напрямую с оперативной памятью электронной книги. Это увеличивает скорость отображения и надежность. Позвольте поделиться опытом рисования в Go на примере генератора штрихкодов и отображении на электронной книге через контроллер Wirenboar 5.

Задачи


  1. Ничего не сломать. Мы только добавляем новую функцию.
  2. Электронная книжка должна уметь слушать порт и в течении 1 секунды выводить изображение
  3. Работа только через буфер в оперативной памяти, никакой flash памяти
  4. Протестировать вывод великой командой dd
  5. Рисование штрихкода нужного размера в Go и размещение его в центре холста
  6. Передача изображения в буфер электронной книжки
  7. Наслаждаться полученным результатом

RAM Drive через USB


В читалке Sony PRS-505 нет wi-fi и к тому же из модулей ядра USB Gadget имеется только g_file_storage, поэтому это единственный способ быстро передать изображение. К счастью прошивка от PRSPlus умеет запускать любой скрипт при включении электронной книги. Все что нам нужно, это просто положить нужные файлы в папку directory/database/system/PRSPlus и при загрузке будет запущен скрипт prsp.sh.

В качестве буфера использовать Flash память нельзя, поэтому нам нужен небольшой tmpfs диск в оперативной памяти который будет доступен по USB, следует выгрузить модуль ядра g_file_storage и загрузить его с нужными параметрами, для публикации нашего созданного RAM диска по USB. Далее мы должны отслеживать изменения в заданной области и выводить изображение на e-ink дисплей.

prsp.sh
#!/bin/sh
echo $'\n===============\nSTART SCRYPT\n' >> /dev/console

#TODO «here need Kernel Event instead while and sleep bottom placed»
function waitnewdata
{
echo $'\n===============\nWait new data\n' >> /dev/console

#Show only modify time the image file
MODIFYTIMEOLD=`ls -l --full-time /tmp/raw.img | awk ' {print $9} '`
MODIFYTIMENEW=$MODIFYTIMEOLD

while [ "$MODIFYTIMEOLD" == "$MODIFYTIMENEW" ]
do
MODIFYTIMENEW=`ls -l --full-time /tmp/raw.img | awk ' {print $9} '`

sleep 0.2
done

if [ "$MODIFYTIMEOLD" != "$MODIFYTIMENEW" ]
then
showpic
fi
}

function showpic
{
echo $'\n===============\nNew data received\n' >> /dev/console

#Generating Back Screen for best clear e-ink (optional)
dd if=/dev/zero of=/tmp/img.raw bs=1k count=480
/tmp/showpic /tmp/img.raw

dd if=/tmp/raw.img of=/tmp/img.raw bs=1k count=480
/tmp/showpic /tmp/img.raw

waitnewdata
}

#ldconfig
PATH="/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games:/usr/local/sony/bin:/usr/sbin:/sbin"
LD_LIBRARY_PATH="/Data/opt/sony/ebook/application:/lib:/usr/lib:/usr/local/sony/lib:/opt/sony/ebook/lib"
export PATH LD_LIBRARY_PATH

# set initial date
/bin/date 0101000007

#Unload kernel module
rmmod g_file_storage

#Create raw file 1Mb
dd if=/dev/zero of=/tmp/raw.img bs=1k count=1k

grep Data /proc/mtd > /dev/null
if [ $? == 0 ]; then

NUM=`grep Data /proc/mtd | awk -F: '{print $1}' | awk -Fd '{print$2}'`
insmod /lib/modules/2.4.17_n12/kernel/drivers/usb/g_file_storage.o file=/dev/mtdblock$NUM,/dev/sdmscard/r5c807b,/dev/sdmscard/r5c807a,/tmp/raw.img ProductID=$MODEL VendorSpecific=$VENDOR sn_select=0 iSerialNumber=$ID
else
insmod /lib/modules/2.4.17_n12/kernel/drivers/usb/g_file_storage.o file=/dev/sdmscard/r5c807b,/dev/sdmscard/r5c807a,/tmp/raw.img ProductID=$MODEL VendorSpecific=$VENDOR sn_select=0 iSerialNumber=$ID
fi

#start kbook application
nohup /opt/sony/ebook/application/tinyhttp > /dev/null &

cp /Data/database/system/PRSPlus/showpic /tmp/

waitnewdata

Основные моменты, что делает скрипт psrp.sh

  • Сначала мы выгружаем модуль:

    rmmod g_file_storage

  • Создаем пустой файл в /tmp размером в 1Мб. Предварительно подключившись к UART консоли ридера, я имел возможность удостовериться что /tmp примонтирован именно в tmpfs. Это то что нам нужно.

    dd if=/dev/zero of=/tmp/raw.img bs=1k count=1k

  • Загружаем модуль g_file_storage добавив /tmp/raw.img

    insmod /lib/modules/2.4.17_n12/kernel/drivers/usb/g_file_storage.o file=/dev/mtdblock$NUM,/dev/sdmscard/r5c807b,/dev/sdmscard/r5c807a,/tmp/raw.img

  • Запускаем родную оболочку tinyhttp, к сожалению без этого диски не будут расшарены по USB

    nohup  /opt/sony/ebook/application/tinyhttp > /dev/null &

  • Запускаем функцию waitnewdata она в цикле с паузой 200мс следит за изменениями в файле образа нашего виртуального диска. По времени изменения

    while [ "$MODIFYTIMEOLD" == "$MODIFYTIMENEW" ]
    do
    MODIFYTIMENEW=`ls -l --full-time /tmp/raw.img | awk ' {print $9} '`
    
    sleep 0.2
    done'

    Да, согласен, знатный велосипедик, но к сожалению в прошивке нет inotify, а с Kernel Event мне было лениво разбираться. Тем более что fps в 1 секунду меня устроит.

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

Теперь наша электронная книжка умеет слушать и при этом, на ней как и раньше можно читать книги. Главное нужно иметь ввиду для того чтобы скрипт запустился и мы могли передавать изображения, при включении электронной книжки USB кабель не должен быть подключен. Иначе книжка загрузится без нашего скрипта prsp.sh. То есть сначала включаем книжку, ждем когда загрузится оболочка потом подключаем USB кабель. (эта особенность по умолчанию прописана в прошивке PRSPlus, при желании это тоже можно изменить, сделав свой собственный образ)

Проверим


Нажимаем reset на электронной книжке, ждем окончания загрузки, подключаем USB кабель. Для проверки мы можем отправить тестовое изображение. Например из Ubuntu это можно сделать так:

Если при загрузке ридера скрипт успешно запустился, то при подключению по USB мы увидим устройство с размером 1Мб.

fdisk -l

Находим вот такую строку:

Disk /dev/sdx: 1 MB, 1048576 bytes

Теперь мы знаем, вот он наш кусочек оперативной памяти электронной книги /dev/sdx.

Для конвертации из jpeg, нам понадобится djpeg, установим нужные пакеты:

apt-get install libjpeg-turbo-progs

Далее создаем JPEG файл в Вашем любимом редакторе, размером 600x800 и отправляем его на электронную книжку.

djpeg -pnm -grayscale test.jpg | dd bs=1 skip=15 | dd of=/dev/sdx bs=480k

В этом конвейере мы преобразуем jpeg в монохромный pgm, пропускаем заголовок и передаем 480Кб единым блоком на устройство /dev/sdx. И тут же видим результат.

Генератор штрих кода и отправка его в устройство


Для рисования штрихкода в Golang нам понадобятся дополнительные библиотеки:

go get github.com/boombuler/barcode
go get golang.org/x/image/bmp

main.go
package main

import (
	"bytes"
	"fmt"
	"image"
	"log"
	"os"

	"image/color"

	"image/draw"

	"golang.org/x/image/bmp"

	"syscall"

	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/ean"
	"github.com/boombuler/barcode/qr"
)

func main() {

	switch string(os.Args[2]) {
	case "qr":
		base64 := os.Args[3]
		log.Println("Original data:", base64)

		code1pixel, err := qr.Encode(base64, qr.L, qr.Unicode)
		if err != nil {
			log.Fatal(err)
		}

		log.Println("Encoded data: ", code1pixel.Content())

		if base64 != code1pixel.Content() {
			log.Fatal("data differs")
		}
		log.Println("Encoded data: ", code1pixel.Content())

		if base64 != code1pixel.Content() {
			log.Fatal("data differs")
		}

		codeScalled, err := barcode.Scale(code1pixel, 300, 200)
		if err != nil {
			log.Fatal(err)
		}
		drtest(codeScalled)

	case "ean":
		// code, err := ean.Encode("123456789012")
		code1pixel, err := ean.Encode(os.Args[3])
		if err != nil {
			log.Fatal(err)
		}

		log.Println("Encoded data: ", code1pixel.Content())

		codeScalled, err := barcode.Scale(code1pixel, 300, 300)
		if err != nil {
			log.Fatal(err)
		}
		drtest(codeScalled)

	}
}

func drtest(imgSrc image.Image) {

	// create a new Image with the same dimension of image
	newImg := image.NewGray(image.Rect(0, 0, 600, 800))

	// we will use white background to replace background
	// you can change it to whichever color you want with

	// a new color.RGBA{} and use image.NewUniform(color.RGBA{<fill in color>}) function
	draw.Draw(newImg, newImg.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)

	// paste image OVER to newImage
	draw.Draw(newImg, newImg.Bounds().Add(image.Point{150, 300}), imgSrc, imgSrc.Bounds().Min, draw.Over)

	buf := new(bytes.Buffer)

	err := bmp.Encode(buf, newImg)

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	send_s3 := buf.Bytes()

	fmt.Println("OK")

	if os.Args[1] != "false" {
		devout(send_s3[1078:])
	}

}

func devout(buffer []byte) {
	// disk := "/dev/sde"
	disk := os.Args[1]
	var fd, numread int
	var err error

	fd, err = syscall.Open(disk, syscall.O_RDWR, 0777)

	if err != nil {
		fmt.Print(err.Error(), "\n")
		return
	}

	//WRITE
	numread, err = syscall.Write(fd, buffer)

	if err != nil {
		fmt.Print(err.Error(), "\n")
	}

	fmt.Printf("Numbytes write: %d\n", numread)
	// fmt.Printf("Buffer: %x\n", buffer[:1000])

	err = syscall.Close(fd)

	if err != nil {
		fmt.Print(err.Error(), "\n")
	}
}

Основные моменты в коде:

  1. На примере EAN, сначала мы рисуем штрих код толщиной в 1 пиксель:

    code1pixel, err := ean.Encode(os.Args[3])

  2. Растянем его до нужных размеров:

    codeScalled, err := barcode.Scale(code1pixel, 300, 300)

  3. Создаем холст 600х800 под размер экрана:

    newImg := image.NewGray(image.Rect(0, 0, 600, 800))

  4. Заполняем его нужным цветом:

    draw.Draw(newImg, newImg.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)

  5. Накладываем на наш холст изображение штрихкода:

    draw.Draw(newImg, newImg.Bounds().Add(image.Point{150, 300}), imgSrc, imgSrc.Bounds().Min, draw.Over)

  6. Далее мы открываем устройство на запись и отправляем туда данные исключив заголовок BMP:

    devout(send_s3[1078:])

Кросс компиляция под Wirenboard 5


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

GOOS=linux GOARCH=arm  GOARM=5 go build main.go

Переносим все на Wirenboard 5:

scp main root@192.168.x.x:/tmp

Переходим на Wirenboard, смотрим имя устройства размером в 1 Мб, в моем примере /dev/sdd.

Запускаем:

/tmp/main /dev/sdd qr "Privet Habr"



Выводы


Использование электронной книги в качестве экрана вполне реально. Своим потенциалом технология электронных чернил побуждает к использованию в дизайне интерьера. E-ink экран будет хорошо смотреться особенно на светлой стене. Можно выводить полезную информацию с домашнего контроллера.

Спасибо за внимание!

P. S. Исходники, можно посмотреть тут и тут. Прошивка PRSPlus для электронных книг тут.
Поделиться с друзьями
-->

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


  1. mgremlin
    06.02.2017 19:22
    +2

    красиво, спасибо.


  1. dlinyj
    08.02.2017 12:54
    +1

    Вот, наконец-то было доделано то, что я не смог сделать. Как я правильно понял книжку вы даже не разбирали :). Я не совсем уловил суть, была использована родная прошивка или сторонняя? И каким образом удалось сделать виртуальный жёсткий диск? Понимаю, что тут важно было написать кратко, как инструкцию, но очень интересны поиски, решения и т.п.

    Очень круто! Ещё, можете показать видео с загрузкой изображения, как быстро это происходит?


    1. alexshnup
      10.02.2017 19:08
      +1

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

      Quick start
      Кстати выложил все что нужно в одном архиве showpic.zip. Там в нем все что нужно. Прошивка PRSPlus и скрипт с бинарником для отображения. Нужно просто распаковать содержимое архива во внутреннюю память книжки и перезагрузить ее. Он попросит обновить прошивку, обновляем потом reset. Когда книжка включается или перезагружается USB должен быть выключен. Потом когда загрузится, подключаем USB, проверяем что скрипт «prsp.sh» и бинарник «showpic» лежат на своем месте. В директории directory/database/system/PRSPlus

      Виртуальный жёсткий диск
      При всех последующих загрузках книжки, если USB не подключен, загрузится скрипт, выгрузит модуль ядра «g_file_storage» и загрузит его уже с нужными нам параметрами (подробности в статье), это и добавляет нам новый диск который ссылается на файл лежащий в оперативной памяти tmpfs. При подключенном USB во время включения или перезагрузке, скрипт не выполняется и книжка запускается обычным способом без нашего скрипта. Это позволяет безболезненно экспериментировать.

      Скорость загрузки
      Из компьютера jpg картинка передается и меняется в течении одной секунды. Отправка единым блоком занимает примерно 200мс. Мне этого достаточно для вывода необходимой информации.


      1. dlinyj
        11.02.2017 17:40

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


  1. alexshnup
    10.02.2017 20:15
    +1

    Видео с загрузкой изображенияния


    1. dlinyj
      11.02.2017 17:42

      Огромное спасибо за видео! Очень шустро работает!