El motor de configuración#
Una configuración residente#
Una única instancia app_config_t g_config (main/app_config.c /
app_config.h) es la copia viva que lee cada subsistema. Se carga al arrancar
y se edita campo a campo por los manejadores POST de la web. Sus campos se
agrupan por página de la administración web: sistema/hora, identidad «My Station»,
Wi-Fi, IGate, BrandMeister, Digipeater, Tracker, Weather, GPS, el módem AFSK,
System/autenticación HTTP, Message, Query y la cuenta Winlink.
Los nombres de campo y las claves JSON se mantienen 1:1 con el config.h/
config.cpp del proyecto de referencia original, de modo que cada valor que
muestra la administración web tiene un hogar y un operador que se mueva entre
ambos proyectos reconoce las claves.
Un archivo por funcionalidad#
La estructura residente es una cosa; dónde se guarda es otra. Cada página de la
administración web con ajustes persistentes tiene un archivo propio bajo
/storage, con el nombre de la página — el menú lateral es el índice de la
lista de archivos:
Archivo |
Página |
|---|---|
|
Sistema: reloj de CPU, servidores SNTP y resincronización, zona horaria, credenciales de la administración web. |
|
My Station: indicativo, posición, datos de antena PHG, ambigüedad, opciones del informe de estado, no-archivo y DAO. |
|
Wireless: selección de interfaz, potencia de TX, canal/SSID/clave del SoftAP, los cinco perfiles de estación. |
|
Radiomodem: modulación AFSK/FSK, FX.25, preámbulo, búferes de TX, temporización CSMA, retención de PTT, limitador de ciclo de trabajo. |
|
IGate: ambos sentidos de pasarela y sus filtros, listas de amigos y de satélites, caché de duplicados, filtros de rango y prefijo, pasarela de mensajes, las cuatro ranuras de servidor APRS-IS, la baliza y su extensión de datos. |
|
BrandMeister: los interruptores de interconexión y la lista de indicativos de pasarela. |
|
Digipeater: la tabla de alias y las reglas de repetición, la baliza y los cuatro presets de ruta compartidos, que se editan en esta página. |
|
Tracker: la baliza, las opciones Mic-E y los parámetros de SmartBeaconing. |
|
Weather: el informe y el mapeo de sensores por campo. |
|
GPS: el interruptor del receptor. |
|
Message: el servicio de mensajería, reintentos, GPIO de alarma y grupos de mensajes. |
|
Winlink: los ajustes de cuenta y sesión (las respuestas van a un archivo
aparte, |
|
Query: los interruptores del respondedor y la baliza de capacidades. |
Cada fila es un valor de app_config_section_t, y la tabla SECTIONS de
app_config.c nombra el archivo y las dos mitades de su códec. El manejador
de guardado de una página llama a app_config_save_section() con su propia
sección, así que guardar el digipeater nunca reescribe el archivo del IGate y
una escritura que falle no puede llevarse por delante los ajustes de otra
funcionalidad. La página My Station es la única que nombra varias: sus
espejos de «Use My Station Data» alcanzan campos de otros cinco servicios, así
que pasa a app_config_save_sections() todas las secciones que tocó — un
guardado parcial dejaría la identidad de la estación repartida entre archivos
que ya no coinciden.
El orden de lectura importa exactamente en un punto: el lector de BrandMeister vuelve a aplicar el enclavamiento del monitor mundial contra la pasarela INET→RF que lleva el archivo del IGate, así que la sección IGate se lee primero.
Nota
Las páginas sin ajustes persistentes propios — Dashboard, Snd/Rcv Msg,
Console Logs, File Storage, About — no tienen archivo. Cuatro subsistemas
mantienen estructuras propias en lugar de campos de app_config_t, y
poseen sus archivos directamente: /storage/telemetry.json,
bulletins.json, objitems.json y telegram.json. La razón es el
tamaño — esas tablas agrandarían significativamente la estructura residente
— pero el resultado es la misma regla: una funcionalidad, un archivo.
Carga y guardado#
Primero los valores por defecto, una sola vez.
app_config_load()rellena toda la estructura desdeapp_config_set_defaults()antes de abrir el primer archivo, y luego lee cada sección encima. Cada lector de sección toma el valor de reserva de cada clave del mismo campo que está por sobrescribir, así que una clave que un archivo no lleva — y una sección cuyo archivo no existe siquiera — conserva su valor por defecto documentado. Esto además mantiene una segundaapp_config_tfuera de la pila de la tarea que carga, que ya tiene a su lado el árbol cJSON de una sección vivo en el heap.Cargado con cJSON, un archivo a la vez, así que el pico de asignación es la sección más grande y no la configuración entera.
Ausente, vacío o corrupto → esa sección se reescribe desde los valores por defecto antes de que la carga retorne. Esto es lo que garantiza que cada funcionalidad tenga un archivo en flash desde el primer arranque, sin que un operador tenga que visitar su página.
Sin memoria → no se escribe nada en absoluto y la carga entera informa fallo.
cJSON_Parse()devuelveNULLtanto para un archivo malo como para una asignación fallida, así quejson_store_read()reescanea el texto con una comprobación que no asigna nada antes de decidir cuál de los dos ocurrió; confundirlos borraría una configuración buena en un arranque cargado. Reescribir las secciones genuinamente ausentes mientras otra sigue sin leerse sería aún peor — persistiría una configuración armada mitad desde flash y mitad desde valores por defecto — así que la pasada no escribe nada ymain.creintenta una vez antes de recurrir al conjunto de fábrica.Guardado por un pequeño escritor JSON token a token (
jw_t/jadd_*) que fluye directamente al archivo, evitando la doble asignación de heap que necesitarían un árbol cJSON completo más su búfer serializado. Un búfer estático desetvbuf()se instala justo trasfopen()para que newlib no asigne perezosamente un búfer stdio grande a mitad de escritura.Atómico, por archivo: escribe
<nombre>.json.tmp, luego renombra. Un guardado de varias secciones toma una sola vez el cerrojo de escritura de todo el sistema de archivos alrededor de la tanda, e intenta cada archivo seleccionado incluso después de que uno haya fallado, así que un sistema de archivos lleno no deja al resto con ajustes que el operador ya reemplazó.
API pública: app_config_set_defaults(), app_config_load(),
app_config_save_section(), app_config_save_sections(),
app_config_save() (todas las secciones), app_config_section_path(),
app_config_factory_reset(), y la instancia viva
extern app_config_t g_config.
Concurrencia: el cerrojo de configuración#
g_config lo escriben campo a campo los manejadores POST de la web (un solo
guardado de ajustes reescribe muchos campos, varios de ellos cadenas/arrays, uno
a uno) mientras tareas de larga ejecución (constructores de balizas, login del
IGate, digipeater, mensaje, meteo, respondedor de consultas) leen esos mismos
campos. Un lector que muestrea
una cadena a mitad de strcpy puede ver un valor roto o transitoriamente sin
terminador NUL y salirse del final del búfer. app_config_lock() /
app_config_unlock() serializan esos dos lados.
Es un estricto cerrojo de hoja: se mantiene solo lo suficiente para copiar los campos necesarios a locales — nunca a través de una llamada bloqueante, E/S, transmisión u otro cerrojo. Los campos escalares (de una sola palabra) son atómicos a nivel de palabra en este MCU y pueden leerse sin cerrojo. Es distinto del mutex de guardado interno (mantenido durante toda la serialización a flash).
Interruptores de módulo en compilación#
app_config.h define un conjunto de macros ENABLE_*; comentar una elimina
su entrada de barra lateral y su página de la imagen:
ENABLE_DASHBOARD ENABLE_MSG_CHAT ENABLE_BULLETINS ENABLE_OBJECTS_ITEMS
ENABLE_STATION ENABLE_RADIO_MODEM ENABLE_MESSAGE ENABLE_IGATE
ENABLE_BRANDMEISTER ENABLE_QUERY ENABLE_DIGIPEATER ENABLE_TRACKER
ENABLE_WEATHER ENABLE_TELEMETRY ENABLE_GPS ENABLE_TELEGRAM
ENABLE_WINLINK ENABLE_LOGS ENABLE_SYSTEM ENABLE_WIRELESS
ENABLE_FILE_STORAGE ENABLE_ABOUT_FIRMWARE
No hay interruptor ENABLE_SENSORS: el marco sensors_local no tiene
deshabilitación en compilación y siempre se compila (sus controladores
individuales están condicionados por sus propias opciones Kconfig
CONFIG_SENSORS_LOCAL_*_DRIVER).
Revelar secretos almacenados: ALLOW_SHOW_PASSWORD#
La última entrada del mismo bloque no es un selector de página sino una bandera de política, y por eso se escribe como un valor en lugar de ajustarse comentando la línea:
#define ALLOW_SHOW_PASSWORD 0
Gobierna todos los secretos que edita la interfaz de administración — la
contraseña del admin web, las contraseñas WiFi del AP y de los clientes, el
passcode APRS-IS, la contraseña de Winlink y el token del bot de Telegram — a
través de los tres ayudantes de web_common: web_password_value() escribe
el campo, web_form_get_password() lo lee de vuelta y
web_password_toggle() escribe la casilla Mostrar contraseña.
En 1 las páginas se comportan como siempre: cada campo llega precargado con
el secreto almacenado y lleva una casilla Mostrar contraseña que convierte la
entrada enmascarada en una de texto plano.
En 0 — el valor por omisión — ningún secreto sale nunca del dispositivo por
una página de administración. Un campo que tiene un secreto detrás se muestra
con el marcador fijo WEB_PASSWORD_MASK (*****); uno que no lo tiene se
muestra vacío, de modo que un secreto sin definir sigue leyéndose como tal. La
casilla no se emite en absoluto, y tampoco el script togglePwd() de
web_send_footer() ni la regla .pwd-show de la hoja de estilos, así que
una compilación hecha de esta manera no lleva ninguna de las tres.
La edición no cambia. En el POST, un campo que todavía lleva el marcador
significa «deja este secreto en paz» y el valor almacenado se conserva intacto
mientras el resto de la página se guarda con normalidad; cualquier otro valor lo
reemplaza, incluida la cadena vacía, así que un secreto se cambia y se borra
exactamente como antes. Lo único que esto cuesta es el literal *****, que no
puede almacenarse como secreto mientras la bandera esté en 0 porque es
indistinguible de un campo sin tocar.
Un detalle útil al leer la página Wireless: sus campos de contraseña llevan
minlength='8', más largo que el marcador, y no por descuido — minlength
solo rige una vez que el campo se editó a mano, así que un formulario sin tocar
envía su marcador de cinco caracteres sin protestar mientras que una contraseña
realmente escrita sigue sujeta a la longitud WPA2 completa.
Presets de ruta y máscaras de bits#
Cada servicio (tracker / igate / digi / wx / …) almacena una máscara de bits,
no una cadena de ruta. El bit N selecciona g_config.path[N], uno de los
cuatro presets de texto libre editados en la página Digi.
aprs_path_build_suffix() concatena cada ranura seleccionada no vacía; las
ranuras seleccionadas-pero-vacías se saltan. Es compartida por las balizas, el
tiempo, la telemetría, los mensajes y las respuestas a consultas, y aplica el
límite AX.25 de 8 vías en el momento de transmitir, de modo que una configuración
que llegó al dispositivo sin pasar por un formulario web no puede poner una ruta
demasiado larga en el aire.
Los Objetos/Ítems son el único servicio que no une las ranuras entre sí: su ruteo
proporcional envía un preset por transmisión y rota por la selección, así que
objitem_paths() arma la lista por su cuenta. Allí el límite de saltos rige
por preset y no sobre la selección completa, y se cuenta con la misma
app_config_path_hop_count() que usan el constructor compartido y el recorte
del guardado: un preset que por sí solo supera el límite queda fuera de la
rotación.
Todos los selectores salen de fábrica seleccionando el preset 0 y nada más
(PATH_PRESET_MASK_DEFAULT), porque g_config.path[0] es la única ranura
con una cadena de fábrica (WIDE1-1,WIDE2-1) y un bit que apunta a una ranura
vacía emitiría la baliza con el destino pelado.
Los bits del filtro del IGate (compartidos por rf2inetFilter e
inet2rfFilter):
MESSAGE 1<<0 · STATUS 1<<1 · TELEMETRY 1<<2 · WEATHER 1<<3 · OBJECT 1<<4
ITEM 1<<5 · QUERY 1<<6 · BUOY 1<<7 · POSITION 1<<8 · OTHER 1<<9
IGATE_FILT_OTHER es el único bit que cubre varios tipos de carga útil a la
vez — capacidades de estación, formatos definidos por el usuario,
radiogoniometría Agrelo, balizas de localizador Maidenhead y la función de mapa
reservada —, y por eso los conjuntos Filter muestran nueve casillas para diez
bits. IGATE_FILT_QUERY tampoco tiene casilla propia: las consultas se
clasifican para que el código de gating pueda nombrarlas, pero el operador las
gobierna desde la página Query. El tráfico de terceros y los datos de prueba
quedan fuera de todos los bits y nunca se retransmiten por lo que diga la
máscara.