|
1 | 1 | <script setup lang="ts"> |
2 | | -import { ref, onMounted } from 'vue'; |
| 2 | +import { ref, onMounted, computed } from 'vue'; |
3 | 3 | import { pedirDatos } from './utilidades/ayudas'; |
| 4 | +import RecorteImagen from './componentes/RecorteImagen.vue'; |
| 5 | +import { diccionarioEs } from './utilidades/diccionario'; |
| 6 | +import { gruposControles } from './utilidades/controles'; |
| 7 | +
|
4 | 8 | interface OpcionCamara { |
5 | 9 | nombre: string; |
6 | 10 | predeterminado: number | number[] | boolean | null; |
7 | | - max: number | null; |
8 | | - min: number | null; |
9 | | - paso: number | null; |
10 | | - tipo: string; |
| 11 | + max: number | number[] | null; |
| 12 | + min: number | number[] | null; |
11 | 13 | } |
| 14 | +
|
| 15 | +// Estado principal |
12 | 16 | const imagen = ref(''); |
13 | | -const listaOpcionesCamara = ref<OpcionCamara[] | null>(null); |
| 17 | +const listaOpcionesCamara = ref<OpcionCamara[]>([]); |
| 18 | +const valoresActuales = ref<Record<string, any>>({}); |
| 19 | +const controlesAgrupados = computed(() => { |
| 20 | + const map: Record<string, OpcionCamara[]> = {}; |
| 21 | +
|
| 22 | + for (const op of listaOpcionesCamara.value) { |
| 23 | + const grupo = Object.entries(gruposControles).find(([n, arr]) => arr.includes(op.nombre))?.[0] || 'Otros'; |
| 24 | +
|
| 25 | + if (!map[grupo]) map[grupo] = []; |
| 26 | + map[grupo].push(op); |
| 27 | + } |
| 28 | +
|
| 29 | + return map; |
| 30 | +}); |
| 31 | +
|
| 32 | +// ────────────────────────────────────────── |
| 33 | +// WEBSOCKET DE VIDEO |
| 34 | +// ────────────────────────────────────────── |
14 | 35 |
|
15 | 36 | onMounted(async () => { |
16 | 37 | const ws = new WebSocket(`ws://enflujo9.local:8000/ws`); |
17 | 38 | ws.onmessage = (evento) => { |
18 | 39 | imagen.value = 'data:image/jpeg;base64,' + evento.data; |
19 | 40 | }; |
20 | 41 |
|
| 42 | + await cargarControles(); |
| 43 | +}); |
| 44 | +
|
| 45 | +// ────────────────────────────────────────── |
| 46 | +// CARGAR CONTROLES DESDE EL BACKEND |
| 47 | +// ────────────────────────────────────────── |
| 48 | +
|
| 49 | +async function cargarControles() { |
21 | 50 | try { |
22 | | - const datosCamara = await pedirDatos<OpcionCamara[]>('http://enflujo9.local:8000/controles'); |
23 | | - console.log('Datos de cámara recibidos:', datosCamara); |
24 | | - if (datosCamara && typeof datosCamara === 'object') { |
25 | | - const opcionesCamaraOrdenados = Object.entries(datosCamara).sort((a, b) => a[0].localeCompare(b[0])); |
26 | | - listaOpcionesCamara.value = opcionesCamaraOrdenados.map(([nombre, opcion]) => ({ |
27 | | - nombre, |
28 | | - predeterminado: opcion.predeterminado ?? null, |
29 | | - max: opcion.max ?? null, |
30 | | - min: opcion.min ?? null, |
31 | | - paso: opcion.paso ?? null, |
32 | | - tipo: opcion.tipo ?? '??', |
33 | | - })); |
34 | | - } else { |
35 | | - console.error('Datos de cámara no válidos:', datosCamara); |
36 | | - } |
| 51 | + const datos = await pedirDatos<Record<string, any>>('http://enflujo9.local:8000/controles'); |
| 52 | +
|
| 53 | + const lista = Object.entries(datos).map(([nombre, opcion]) => ({ |
| 54 | + nombre, |
| 55 | + predeterminado: opcion.predeterminado, |
| 56 | + min: opcion.min, |
| 57 | + max: opcion.max, |
| 58 | + })); |
| 59 | +
|
| 60 | + // Ordenar alfabéticamente |
| 61 | + lista.sort((a, b) => a.nombre.localeCompare(b.nombre)); |
| 62 | +
|
| 63 | + // 1. Filtrar los que no queremos acá |
| 64 | + const sinScaler = lista.filter((op) => op.nombre !== 'ScalerCrop'); |
| 65 | +
|
| 66 | + // 2. Ordenar según el orden de grupos |
| 67 | + listaOpcionesCamara.value = sinScaler.sort((a, b) => { |
| 68 | + const getGrupo = (nombre: string) => |
| 69 | + Object.entries(gruposControles).find(([_, items]) => items.includes(nombre))?.[0] || 'Z_otros'; |
| 70 | +
|
| 71 | + const ga = getGrupo(a.nombre); |
| 72 | + const gb = getGrupo(b.nombre); |
| 73 | +
|
| 74 | + if (ga === gb) return a.nombre.localeCompare(b.nombre); |
| 75 | +
|
| 76 | + return ga.localeCompare(gb); |
| 77 | + }); |
| 78 | +
|
| 79 | + // Estado actual editable |
| 80 | + lista.forEach((op) => { |
| 81 | + valoresActuales.value[op.nombre] = Array.isArray(op.predeterminado) ? [...op.predeterminado] : op.predeterminado; |
| 82 | + }); |
37 | 83 | } catch (error) { |
38 | | - console.error('Error al obtener los datos de la cámara:', error); |
| 84 | + console.error('Error cargando controles:', error); |
39 | 85 | } |
40 | | -}); |
| 86 | +} |
| 87 | +
|
| 88 | +async function enviarControl(nombre: string) { |
| 89 | + const valor = valoresActuales.value[nombre]; |
| 90 | +
|
| 91 | + const res = await fetch('http://enflujo9.local:8000/controlar', { |
| 92 | + method: 'POST', |
| 93 | + headers: { 'Content-Type': 'application/json' }, |
| 94 | + body: JSON.stringify({ nombre, valor }), |
| 95 | + }); |
| 96 | +
|
| 97 | + const data = await res.json(); |
| 98 | + console.log('Control cambiado:', data); |
| 99 | +} |
| 100 | +
|
| 101 | +async function restaurarPredeterminados() { |
| 102 | + const payload: Record<string, any> = {}; |
| 103 | +
|
| 104 | + listaOpcionesCamara.value.forEach((op) => { |
| 105 | + // Saltar controles que no tienen predeterminado válido |
| 106 | + if (op.predeterminado === null || op.predeterminado === undefined) { |
| 107 | + return; |
| 108 | + } |
| 109 | +
|
| 110 | + payload[op.nombre] = Array.isArray(op.predeterminado) ? [...op.predeterminado] : op.predeterminado; |
| 111 | + }); |
| 112 | +
|
| 113 | + console.log('Restaurando:', payload); |
| 114 | +
|
| 115 | + const res = await fetch('http://enflujo9.local:8000/controlar-multiples', { |
| 116 | + method: 'POST', |
| 117 | + headers: { 'Content-Type': 'application/json' }, |
| 118 | + body: JSON.stringify(payload), |
| 119 | + }); |
| 120 | +
|
| 121 | + const data = await res.json(); |
| 122 | + console.log('Restaurado:', data); |
| 123 | +
|
| 124 | + // Actualizar la UI solo con controles válidos |
| 125 | + Object.entries(payload).forEach(([nombre, valor]) => { |
| 126 | + valoresActuales.value[nombre] = Array.isArray(valor) ? [...valor] : valor; |
| 127 | + }); |
| 128 | +} |
| 129 | +
|
| 130 | +function descripcionDe(nombre: string): string { |
| 131 | + return diccionarioEs[nombre] ?? 'Sin descripción disponible'; |
| 132 | +} |
41 | 133 | </script> |
42 | 134 |
|
43 | 135 | <template> |
44 | | - <img :src="imagen" alt="Cámara" /> |
45 | | - <table id="opcionesCamara"> |
46 | | - <thead> |
47 | | - <tr> |
48 | | - <th>Nombre</th> |
49 | | - <th>Tipo</th> |
50 | | - <th>Valor predeterminado</th> |
51 | | - <th>Mínimo</th> |
52 | | - <th>Máximo</th> |
53 | | - <th>Paso</th> |
54 | | - </tr> |
55 | | - </thead> |
56 | | - <tbody class="opciones"> |
57 | | - <tr v-for="obj in listaOpcionesCamara" :key="obj.nombre"> |
58 | | - <td class="nombre">{{ obj.nombre }}</td> |
59 | | - <td class="tipo">{{ obj.tipo }}</td> |
60 | | - <td class="valor">{{ obj.predeterminado }}</td> |
61 | | - <td>{{ obj.min }}</td> |
62 | | - <td>{{ obj.max }}</td> |
63 | | - <td>{{ obj.paso }}</td> |
64 | | - </tr> |
65 | | - </tbody> |
66 | | - </table> |
| 136 | + <div class="contenedor"> |
| 137 | + <img class="preview" :src="imagen" alt="Cámara" /> |
| 138 | + <RecorteImagen /> |
| 139 | + <button @click="restaurarPredeterminados">Restaurar predeterminados</button> |
| 140 | + |
| 141 | + <div class="panel"> |
| 142 | + <h2>Controles de cámara</h2> |
| 143 | + |
| 144 | + <div v-for="(items, grupo) in controlesAgrupados" :key="grupo" class="grupo"> |
| 145 | + <h2>{{ grupo }}</h2> |
| 146 | + <div v-for="op in items" :key="op.nombre" class="control"> |
| 147 | + <div class="nombre"> |
| 148 | + <h3>{{ op.nombre }}</h3> |
| 149 | + <span class="descripccion"> |
| 150 | + {{ descripcionDe(op.nombre) }} |
| 151 | + </span> |
| 152 | + </div> |
| 153 | + |
| 154 | + <!-- BOOLEANOS --> |
| 155 | + <template v-if="typeof op.predeterminado === 'boolean'"> |
| 156 | + <label> |
| 157 | + <input type="checkbox" v-model="valoresActuales[op.nombre]" @change="enviarControl(op.nombre)" /> |
| 158 | + {{ valoresActuales[op.nombre] ? 'On' : 'Off' }} |
| 159 | + </label> |
| 160 | + </template> |
| 161 | + |
| 162 | + <!-- NÚMEROS --> |
| 163 | + <template v-else-if="typeof op.predeterminado === 'number' || typeof valoresActuales[op.nombre] === 'number'"> |
| 164 | + <input |
| 165 | + type="range" |
| 166 | + :min="typeof op.min === 'number' ? op.min : undefined" |
| 167 | + :max="typeof op.max === 'number' ? op.max : undefined" |
| 168 | + step="0.1" |
| 169 | + v-model.number="valoresActuales[op.nombre]" |
| 170 | + @input="enviarControl(op.nombre)" |
| 171 | + /> |
| 172 | + <input |
| 173 | + class="num" |
| 174 | + type="number" |
| 175 | + :min="typeof op.min === 'number' ? op.min : undefined" |
| 176 | + :max="typeof op.max === 'number' ? op.max : undefined" |
| 177 | + step="0.1" |
| 178 | + v-model.number="valoresActuales[op.nombre]" |
| 179 | + @change="enviarControl(op.nombre)" |
| 180 | + /> |
| 181 | + </template> |
| 182 | + |
| 183 | + <!-- LISTAS / TUPLAS --> |
| 184 | + <template v-else-if="Array.isArray(op.predeterminado)"> |
| 185 | + <div class="lista"> |
| 186 | + <div class="item" v-for="(_, i) in valoresActuales[op.nombre]" :key="i"> |
| 187 | + <input |
| 188 | + type="number" |
| 189 | + step="0.1" |
| 190 | + v-model.number="valoresActuales[op.nombre][i]" |
| 191 | + @change="enviarControl(op.nombre)" |
| 192 | + /> |
| 193 | + </div> |
| 194 | + </div> |
| 195 | + </template> |
| 196 | + |
| 197 | + <!-- FALLBACK --> |
| 198 | + <template v-else> |
| 199 | + <span>No editable</span> |
| 200 | + </template> |
| 201 | + </div> |
| 202 | + </div> |
| 203 | + </div> |
| 204 | + </div> |
67 | 205 | </template> |
68 | 206 |
|
69 | | -<style lang="scss" scoped> |
70 | | -#opcionesCamara { |
71 | | - list-style-type: none; |
72 | | - padding: 0; |
73 | | - margin: 0; |
| 207 | +<style scoped> |
| 208 | +.contenedor { |
| 209 | + display: flex; |
| 210 | + gap: 20px; |
| 211 | +} |
74 | 212 |
|
75 | | - .opciones { |
76 | | - .tipo { |
77 | | - font-style: italic; |
78 | | - color: #555; |
79 | | - } |
| 213 | +.preview { |
| 214 | + width: 640px; |
| 215 | + height: 480px; |
| 216 | + background: black; |
| 217 | +} |
80 | 218 |
|
81 | | - .nombre { |
82 | | - font-weight: bold; |
83 | | - color: #000; |
84 | | - } |
| 219 | +.panel { |
| 220 | + max-height: 480px; |
| 221 | + overflow-y: auto; |
| 222 | + padding-right: 10px; |
| 223 | +} |
85 | 224 |
|
86 | | - .valor { |
87 | | - color: #007bff; |
88 | | - } |
89 | | - } |
| 225 | +.control { |
| 226 | + margin-bottom: 15px; |
| 227 | + padding: 8px; |
| 228 | + border-bottom: 1px solid #ddd; |
| 229 | +} |
| 230 | +
|
| 231 | +.nombre h3 { |
| 232 | + margin-bottom: 0; |
| 233 | +} |
| 234 | +
|
| 235 | +.descripccion { |
| 236 | + font-weight: normal; |
| 237 | + font-size: 0.9em; |
| 238 | + color: #555; |
| 239 | + font-style: italic; |
| 240 | + margin-bottom: 2em; |
| 241 | +} |
| 242 | +
|
| 243 | +.lista { |
| 244 | + display: flex; |
| 245 | + gap: 5px; |
| 246 | +} |
| 247 | +
|
| 248 | +.num { |
| 249 | + width: 70px; |
90 | 250 | } |
91 | 251 | </style> |
0 commit comments