Saltar a contenido

Kiosk Luckfox (port nativo C/LVGL)

El kiosk Luckfox es la segunda variante de la terminal Smiley: el mismo producto de opinión del público, pero corriendo sobre un hardware que no tiene Android. El kiosk Smiley original es una app Android/Jetpack Compose; el panel Luckfox usa un SoC Rockchip RV1106 con Buildroot Linux, así que la UI se reescribió nativa en C sobre LVGL, hablando el mismo protocolo del backend (/api/v1/smiley/*).

Vive en el repo workdone_luckyfox. Para el visitante es idéntico al kiosk Android: toca una de tres caritas y listo.

Panel Luckfox mostrando la pantalla de opinión con las tres caritas El panel físico de 4" en operación: "¿Cómo encontraste este sanitario?", las tres caritas (Bien / Más o menos / Mal), y abajo la línea de diagnóstico (WiFi, backend, terminal, versión) + QR y marca Lubeca.

Dos kiosks, un solo contrato

El Android (workdone-smiley-kiosk) y el Luckfox (workdone_luckyfox) deben comportarse idénticamente a nivel UI/UX y hablan el mismo SYNC_PROTOCOL_SMILEY. El port C se escribió desde las fuentes Android, citando el archivo Kotlin de cada valor copiado, para poder revisar la paridad. Ver la tabla de diferencias más abajo.

Hardware

Item Valor
Placa Luckfox Pico 86Panel W (RV1106G3 / 256 MB / wireless)
SoC Rockchip RV1106G3 — ARM Cortex-A7 single-core (uClibc), co-proc RISC-V, NPU
RAM 256 MB DDR3L (~236 MB usables)
Display 4" IPS, 720×720, 32 bpp, interfaz RGB (/dev/fb0)
Touch Goodix capacitivo, I²C (/dev/input/event0)
Audio Codec interno RV1106, I²S, playback en /dev/snd/pcmC0D0p
Cámara Ninguna (el panel no tiene sensor)
Conectividad WiFi (wlan0), Ethernet, USB-C gadget (RNDIS + ADB), RS485, 2 relés
Alimentación USB-C 5V (banco) o 6–30 V DC (instalación en pared)

Particiones (eMMC)

/ (rootfs, 5.8 GB, rw) contiene el binario /root/caritas. /userdata (238 MB, rw, journaled) guarda los datos persistentes: la base SQLite, la config y el CA. Ambas sobreviven a corte de energía (ext4 journaled).

Arquitectura

Un solo proceso (caritas) con dos threads que nunca se bloquean entre sí:

flowchart LR
    subgraph caritas["Proceso caritas"]
        UI["UI thread<br/>(main.c + LVGL)<br/>nunca bloquea en I/O"]
        NET["Network thread<br/>(net.c)<br/>todo HTTP/TLS"]
    end
    DB[("SQLite<br/>THREADSAFE=1<br/>cola + config")]
    UI <-->|"condvar net_request_sync"| NET
    UI --> DB
    NET --> DB
    NET <-->|"HTTPS (mbedTLS)"| API["Backend WorkDone<br/>/api/v1/smiley/*"]

La clave: una red mala o caída nunca puede congelar la pantalla — todo el I/O de red vive en su propio thread.

Módulos (src/)

Archivo Responsabilidad
main.c init (fbdev, LVGL, DB, TZ), arranca UI + network thread
ui.c todas las pantallas + máquina de estados, menú técnico, brillo, sonido
faces.c dibuja las tres caritas y la marca Smiley
fbdev.c / evdev.c drivers mínimos de framebuffer y touch
db.c SQLite: cola de opiniones, cache de config, catálogo de motivos; endurecido contra corte de energía
http.c cliente HTTP/1.1 sobre sockets; HTTPS vía mbedTLS con verificación de certificado; decodifica chunked
sync.c sync_run (push/drain/apply deltas) + prov_vincular (pairing)
net.c network thread, resolución del backend, scheduling del sync

Dependencias vendorizadas (third_party/)

Todo se compila con el toolchain uClibc del SDK — sin sorpresas de librerías en runtime: sqlite (amalgamation), cjson, mbedtls 3.6.7 LTS, bcrypt (verifica el PIN técnico contra el hash del backend).

El flujo de opinión

Portado fielmente del OpinionViewModel de Android:

stateDiagram-v2
    [*] --> Caritas
    Caritas --> Gracias: tap POSITIVA o NEUTRA
    Caritas --> Motivos: tap NEGATIVA (si mostrar_motivos)
    Motivos --> Gracias: tap motivo u omitir
    Motivos --> Gracias: timeout 10s
    Gracias --> Caritas: 2s (input bloqueado hasta 3s desde el tap)
  • El client_uuid + ts_local_device se generan al tap, antes de cualquier I/O (garantía de idempotencia).
  • La opinión se persiste en SQLite (con fsync) antes de mostrar "¡Gracias!" — un "gracias" mostrado siempre significa un voto guardado.
  • Tras cada opinión, la UI le avisa al network thread que sincronice ya.
Pantalla de motivos tras una opinión negativa
Motivos (solo si la opinión fue negativa y mostrar_motivos está activo).
Pantalla de agradecimiento
"¡Gracias!" ~2s y vuelve a las caritas.

Reposo, menú técnico y endurecimiento

En reposo el panel muestra: la marca Smiley (arriba izq.), reloj en vivo (TZ −03), la línea de diagnóstico (terminal / sanitario / versión), un QR al sitio visual, y un indicador WiFi tricolor (rojo = sin WiFi, azul = asociado, verde = backend alcanzable). En modo atracción, las tres caritas hacen un bob suave para invitar al toque. El brillo es pleno en horario operativo (07–22) y atenuado de noche — nunca se apaga por idle (un kiosk debe seguir visible).

Menú técnico (oculto): 10 toques sobre el logo Smiley en 2,5 s → PIN → menú (brillo, sonido, WiFi, URL del backend, re-vincular, reiniciar, cerrar).

Menú técnico del panel
Menú técnico: brillo, sonido, WiFi, servidor, re-vincular, reiniciar.
Pantalla de ingreso de PIN técnico
Ingreso del PIN técnico (bcrypt contra el hash del backend).

Endurecimiento kiosk:

  • Watchdog de reinicio ante crash (S99smiley: while true; do /root/caritas; sleep <backoff>; done con backoff escalado).
  • Reboot nocturno a las 04:00 (limpia el log en tmpfs; se desactiva con auto_reboot=0).
  • Votos durables ante corte de energía: SQLite synchronous=FULL + journal_mode=TRUNCATE + journal de ext4. Verificado con un corte físico real.

Sincronización

Habla el mismo SYNC_PROTOCOL_SMILEY que el kiosk Android (ver Sincronización y API). Pull-based, JSON sobre HTTPS, auth por headers X-Device-Uuid + X-Api-Key:

Endpoint Para qué
GET /ping chequeo de conexión (público)
POST /vincular {codigo} pairing → device_uuid, api_key, identidad legible
POST /sync sube opiniones pendientes (cronológico), aplica deltas (motivos) + config, drena las que el server ackeó (OK / DUPLICADO_IGNORADO)
GET /estado refresco liviano del "última limpieza"

Detrás de Cloudflare

Como el backend está detrás de Cloudflare, las respuestas HTTPS vienen chunked — el cliente decodifica el transfer-encoding chunked.

Configuración

La config vive en SQLite (config_local, key/value) y en archivos de /userdata/:

Clave La setea Significado
device_uuid, api_key vincular credenciales del dispositivo
sanitario_nombre, numero_terminal, alias vincular identidad legible
pin_tecnico_hash sync (backend) hash bcrypt del PIN técnico
mostrar_motivos, ultima_limpieza_hace_min, … sync (backend) config remota
auto_brillo, sonido_local menú horario de brillo / beep del tap
auto_reboot manual 0 desactiva el reboot nocturno

Archivos de /userdata/: smiley.db (la base), backend.conf (override de la URL del backend, ausente = producción), cacert.pem (override del CA, ausente = bundle embebido).

Build y deploy

Requiere WSL2 Ubuntu 22.04

El SDK de Luckfox solo soporta Ubuntu 22.04 x86_64. En Windows eso significa WSL2. El SDK se clona aparte (/home/pi/luckfox-pico), no está vendorizado en el repo.

# dentro de WSL, desde la raíz del repo
make -j16                                   # produce build/caritas (ELF ARM uClibc)

# deploy por USB (adb). En Git Bash: export MSYS_NO_PATHCONV=1
adb push build/caritas /root/caritas
adb shell 'chmod +x /root/caritas; killall caritas'   # el watchdog lo relanza

device/install.sh hace la instalación completa (binario + init script, deshabilita la demo de fábrica). El paso a paso está en la guía de deploy.

Recetas de operación (rápidas)

  • SSH/consola: por el cable USB, ssh root@169.254.148.50 (IP link-local que setea S99smiley al boot). Cambiá el password de root en producción.
  • WiFi: menú → Configurar WiFi.
  • Backend de prueba: menú → Servidor → http://<ip>:<port> (permite http:// plano, saltea TLS). O echo "http://..." > /userdata/backend.conf && killall caritas.
  • PIN técnico: se administra en el BackOffice (baja como pin_tecnico_hash); fallback pin_local, y por último el default 1234.

Seguridad

  • Verificación TLS activa (VERIFY_REQUIRED): un certificado forjado o self-signed se rechaza (probado contra expired.badssl.com / self-signed.badssl.com).
  • PIN verificado con bcrypt real contra el hash del backend.
  • La api_key se guarda en texto plano en la eMMC — el RV1106 no tiene keystore por hardware. Mitigable, no eliminable sin un secure element.

Diferencias vs el kiosk Android

Mismo producto, mismo contrato; lo que cambia es la plataforma:

Aspecto Kiosk Android (smiley-kiosk) Kiosk Luckfox (este)
Hardware Tablet Android Luckfox RV1106, Buildroot Linux
UI Kotlin + Jetpack Compose C nativo + LVGL
Persistencia Room (SQLite) SQLite (amalgamation, C)
TLS stack de Android mbedTLS embebido
Modo kiosko Lock Task (Android) watchdog S99smiley + init de Buildroot
Deploy APK (gradlew installDebug) make en WSL + adb push del ELF
Protocolo backend idéntico (SYNC_PROTOCOL_SMILEY) idéntico
Flujo de opinión idéntico (paridad revisada) idéntico

Bug de backend detectado desde el port (a verificar)

El equipo del port reportó que POST /api/v1/smiley/sync responde OK pero no persiste el ultimo_sync de la terminal — la tablet Android de referencia muestra el mismo síntoma, así que el bug está en SmileySyncService (server-side), no en el dispositivo. Vale registrarlo como los otros riesgos del backend.