...

понедельник, 6 февраля 2017 г.

[Из песочницы] Электронные чернила для Wirenboard 5 или рисуем штрихкоды на Go

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

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 http://ift.tt/2b6YSKx
go get http://ift.tt/1IMXSQP

main.go
package main

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

        "image/color"

        "image/draw"

        "http://ift.tt/1IMXSQP"

        "syscall"

        "http://ift.tt/2b6YSKx"
        "http://ift.tt/2la2G0S"
        "http://ift.tt/2leMLva"
)

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 для электронных книг тут.

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

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий