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.
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_devicese 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.
mostrar_motivos está activo).
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).
Endurecimiento kiosk:
- Watchdog de reinicio ante crash (
S99smiley:while true; do /root/caritas; sleep <backoff>; donecon 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 seteaS99smileyal boot). Cambiá el password de root en producción. - WiFi: menú → Configurar WiFi.
- Backend de prueba: menú → Servidor →
http://<ip>:<port>(permitehttp://plano, saltea TLS). Oecho "http://..." > /userdata/backend.conf && killall caritas. - PIN técnico: se administra en el BackOffice (baja como
pin_tecnico_hash); fallbackpin_local, y por último el default1234.
Seguridad¶
- Verificación TLS activa (
VERIFY_REQUIRED): un certificado forjado o self-signed se rechaza (probado contraexpired.badssl.com/self-signed.badssl.com). - PIN verificado con bcrypt real contra el hash del backend.
- La
api_keyse 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.