Buenas noches frías, húmedas, grises, neblinosas... un día de gran afluencia de rioplatenses al foro
.
Estaba pensando en una interfaz de comandos hecha a mi gusto, y llego al punto donde no me gusta ninguna de las opciones que tengo.
A ver si alguno da con un argumento que defina la contienda.
Problema:
Estoy haciendo una interfaz de comandos típica, donde uno escriba en una consola de puerto serie algo como:
LED ON
aprieta enter y el led se enciende.
O si pongo
LED TOGGLE 200
el led se enciende y se apaga (parpadea) cambiando de estado cada 200 milisegundos.
¿Simple no?.
Bueno, las cosas tienen su forma de complicarse cuando se quiere trabajar con una interfaz cableada a la par de una interfaz bluetooth, donde queremos que por comandos se pueda acceder a recursos compartidos que pueden no estar disponibles al momento de dar enter, donde queremos tener un modo binario (para nuestro futuro software de puerto serie que nunca va a llegar) y un modo texto (para tipear palabritas en una consola a puerto serie), cuando empezamos a manejar argumentos, etc
No importa, vamos a la consulta en sí. En última instancia el comando led toggle se traduce a un paquete de bytes con esta estructura:
recurso (1 byte) | función (1 byte) | argumentos (opcional) (N bytes)
De alguna forma termino tirando un evento a una cola de eventos, y el main en cuando puede termina llamando al manejador de eventos correspondiente, digamos que se llama CliProcessCmdPackage().
Las opciones son:
Las desventajas de esta implementación me parece que saltan a la vista:
Ventajas:
Cosa que con los punteros a función realizarla requeriría definir una función por cada recurso (si hace falta) y sería más ad-hoc.
Notar también que incluí MemWakeUp(); para decir que la memoria puede ser una flash externa y que para operarla primero preciso sacarla de sleep con un comando si es que no está siendo utilizada por otra parte del programa. Es decir, puedo realizar fácilmente acciones comunes antes de llamar a cualquier función del recurso X.
Finalmente (que manera de escribir
), un híbrido podría ser separar los switch case.
A lo mejor el último caso es menos proclive a la "spaghetización" y me permite combinar lo mejor de los 2 casos?, no sé.
Escucho ofertas
Estaba pensando en una interfaz de comandos hecha a mi gusto, y llego al punto donde no me gusta ninguna de las opciones que tengo.
A ver si alguno da con un argumento que defina la contienda.
Problema:
Estoy haciendo una interfaz de comandos típica, donde uno escriba en una consola de puerto serie algo como:
LED ON
aprieta enter y el led se enciende.
O si pongo
LED TOGGLE 200
el led se enciende y se apaga (parpadea) cambiando de estado cada 200 milisegundos.
¿Simple no?.
Bueno, las cosas tienen su forma de complicarse cuando se quiere trabajar con una interfaz cableada a la par de una interfaz bluetooth, donde queremos que por comandos se pueda acceder a recursos compartidos que pueden no estar disponibles al momento de dar enter, donde queremos tener un modo binario (para nuestro futuro software de puerto serie que nunca va a llegar) y un modo texto (para tipear palabritas en una consola a puerto serie), cuando empezamos a manejar argumentos, etc
No importa, vamos a la consulta en sí. En última instancia el comando led toggle se traduce a un paquete de bytes con esta estructura:
recurso (1 byte) | función (1 byte) | argumentos (opcional) (N bytes)
De alguna forma termino tirando un evento a una cola de eventos, y el main en cuando puede termina llamando al manejador de eventos correspondiente, digamos que se llama CliProcessCmdPackage().
Las opciones son:
- Hacer un doble switch-case bien grandote
- Usar punteros a función
- Hacer dos switch-case por separado
- Otro?
PHP:
typedef enum {
RESOURCE_LED = 0, RESOURCE_BUZZER, RESOURCE_MEM,
}resourceId_t;
typedef enum {
LED_ON = 0, LED_OFF, LED_TOGGLE,
}ledFunction_t;
typedef enum {
//...
}buzzerFunction_t;
typedef enum {
//...
}memFunction_t;
cmdResult_t CliProcessCmdPackage(const uint8_t *package) {
uint8_t resourceId = package[0];
uint8_t functionId = package[1];
cmdResult_t cmdResult = CMD_EXE_OK;
switch(resourceId) {
case RESOURCE_LED:
switch(functionId) {
case LED_ON:
//...
break;
case LED_OFF:
//...
break;
case LED_TOGGLE:
//...
break;
default:
cmdResult = CMD_EXE_UNKNOWN_FUNCTION;
}
break;
case RESOURCE_BUZZER:
switch(functionId) {
case BUZZER_TONE:
//...
break;
case BUZZER_SONG:
//...
break;
default:
cmdResult = CMD_EXE_UNKNOWN_FUNCTION;
}
break;
case RESOURCE_MEM:
switch(functionId) {
case MEM_WRITE:
//...
break;
case MEM_READ:
//...
break;
case MEM_ERASE:
//...
break;
default:
cmdResult = CMD_EXE_UNKNOWN_FUNCTION;
}
break;
default:
cmdResult = CMD_EXE_UNKNOWN_RESOURCE;
}//switch(resourceId)
return cmdResult;
}
- Es un spaghetti de código, una función enorme que crece rápido y te hace perdir de vista que es lo que querés que el comando haga.
- Pérdida de modularidad: esta todo junto, en 1 solo archivo tengo que hacer un include de todos los recursos utilizados
- Sencillo, el compilador si las constantes de identificadores están definidas va a optimizar esto con tabla de saltos, y la ejecución va a ser relativamente rápida (si el compilador hace las cosas bien, cosa que es un riesgo asumirlo).
- Puedo usar comandos que usen varios recursos a la vez, ejemplo, que reproduzca con el buzzer una tonada guardada en memoria y que encienda un led cuando haya una reproducción en curso.
- El manejo de errores es bastante simple (sentencias default).
- Sencillo agregar comandos para nuevos recursos, y nuevas funciones (se agrandan los typedef enums)
PHP:
typedef cmdResult_t (cmdFunction_t *)(void *args);
//la definición y/o la declaración pueden estar en un archivo .c/.h diferente
extern cmdResult_t CmdLedOn(void *args);
extern cmdResult_t CmdLedOff(void *args);
extern cmdResult_t CmdLedToggle(void *args);
const cmdFunction_t ledFunctions[] = {&CmdLedOn, &CmdLedOff, &CmdLedToggle} ;
//...prototipos buzzer...
const cmdFunction_t buzzerFunctions[] = {&CmdBuzzerTone, &CmdBuzzerSong} ;
//...prototipos mem...
//en realidad sería mejor definir las tablas de funciones así, para no
//preocuparse por el orden y que el compilador avise si hay error en tabla
typedef enum {
CMD_MEM_READ = 0, CMD_MEM_WRITE, CMD_MEM_ERASE,
CMD_MEM_MAX
}memFunction_t;
const cmdFunction_t memFunctions[CMD_MEM_MAX] = {
[CMD_MEM_READ] = &CmdMemRead, [CMD_MEM_WRITE] = &CmdMemWrite,
[CMD_MEM_ERASE] = &cmdMemErase
} ;
//--------------------------------------------
const cmdFunction_t * const cmdResources[] = {&ledFunctions, &buzzerFunctions, &memFunctions};
//no se si esta bien esta definición, pero la idea es no ocupar ram
cmdResult_t CliProcessCmdPackage(const uint8_t *package) {
uint8_t resourceId = package[0];
uint8_t functionId = package[1];
//chequear si resourceId es valido
//chequear si functionId es valido
return (
(cmdResources[resourceId][functionId])(args)
);
}
- No hay spaghetti de código
- Cada comando tiene su función, agregar un nuevo comando es agregar una nueva función (modularidad). Puedo hacer 1 archivo para los comandos de memoria, otro para los comandos de led; o poner todo juntos, como uno quiera
- Chequear si functionId es valido puede requerir tener otro arreglo con los tamaños de cada tabla de recursos
- Declaración de un nuevo comando puede ser más engorrosa, hay que tocar no solo los typedef enums, sino también las tablas.
- Depurar puede ser más complicado porque el depurador no sabe a priori cual función se va a llamar, donde con los switch-case pasa lo contrario.
PHP:
cmdResult_t CliProcessCmdPackage(const uint8_t *package) {
uint8_t resourceId = package[0];
uint8_t functionId = package[1];
cmdResult_t cmdResult = CMD_EXE_OK;
switch(resourceId) {
//....otros recursos
case RESOURCE_MEM:
if( mem_busy() ) {
cmdResult = CMD_EXE_RESOURCE_BUSY_ERROR;
} else {
MemWakeUp();
switch(functionId) {
case MEM_WRITE:
//...
break;
case MEM_READ:
//...
break;
case MEM_ERASE:
//...
break;
default:
cmdResult = CMD_EXE_UNKNOWN_FUNCTION;
}
}
break;
//....otros recursos
}
return cmdResult;
}
Notar también que incluí MemWakeUp(); para decir que la memoria puede ser una flash externa y que para operarla primero preciso sacarla de sleep con un comando si es que no está siendo utilizada por otra parte del programa. Es decir, puedo realizar fácilmente acciones comunes antes de llamar a cualquier función del recurso X.
Finalmente (que manera de escribir
PHP:
cmdResult_t CliProcessCmdPackage(const uint8_t *package) {
uint8_t resourceId = package[0];
uint8_t functionId = package[1];
cmdResult_t cmdResult = CMD_EXE_OK;
switch(resourceId) {
case RESOURCE_LED:
cmdResult = CmdLed(functionId, args);
break;
case RESOURCE_BUZZER:
cmdResult = CmdBuzzer(functionId, args);
break;
case RESOURCE_MEM:
cmdResult = CmdMem(functionId, args);
break;
default:
cmdResult = CMD_EXE_UNKNOWN_RESOURCE;
}//switch(resourceId)
return cmdResult;
}
//Y en algun otro archivo o en el mismo definir
cmdResult_t CmdLed(uint8_t functionId, void *args) {
cmdResult_t cmdResult = CMD_EXE_OK;
//aca puedo ejecutar codigo comun para chequear disponibilidad recurso
switch(functionId) {
case LED_ON:
//...
break;
case LED_OFF:
//...
break;
case LED_TOGGLE:
//...
break;
default:
cmdResult = CMD_EXE_UNKNOWN_FUNCTION;
}
return cmdResult;
}
Escucho ofertas