Tworzenie sterowania robotem, jak rozdzielić klasy.

0

Cześć,
Robię sobie robota i zacząłem się zastanawiać nad rozbiciem na klasy jego funkcji. Na pewno będzie jeździł, robił zdjęcia i mówił. W przyszłości dojdą jeszcze inne funkcje więc proszę traktować moje pytanie ogólnikowo.
Dodam jeszcze że będą różne "kontrolery" tj. apka na telefon, jakiś dedykowany pad który będzie działał przez bluetooth/podczerwień/wifi(najmniej prawdopodobne). Całość będzie pisana w pythonie.
Jak rozbilibyście przekazywanie mu komend?
Według mnie najoptymalniejszym rozwiązaniem będzie:
-klasa Motor - odpowiada za ustawianie odpowiednich wartości na pinach, które są połączone z mikrokontrolerem a ten z silnikami.
-klasa Motor_describer - odpowiada za przyjmowanie komend typu forward, left itp. i tłumaczy je na konkretne wartości, a następnie przekazuje do klasy Motor

Przy robieniu zdjęć/filmów podobny układ.
Przy mówieniu dałbym tylko jedną klasę, której funkcja przyjmuje tylko jeden argument jakim jest string z tym co ma powiedzieć a potem przesyła to do jakiegoś silnika TTS.

Czy moje myślenie jest dobre, czy raczej zrobilibyście to w inny sposób?

1

W Twoim przypadku wyróżnić można dwie warstwy:

  1. Warstwa sprzętowa/sterownikowa, zajmująca się bezpośrednią komunikacją ze sprzętem.
  2. Warstwa obsługi poleceń, cóż, obsługująca polecenia.

Między te dwie warstwy dorzuciłbym jeszcze taką pośrednią, grupującą polecenia w odrębne klasy i możemy pisać ładny kod w stylu:

3.warstwa sprzętowa

class MotorDriver {
	
protected:
	const int
		LEFT_MOTOR_PIN = 10,
		RIGHT_MOTOR_PIN = 11;
		
public:
	static void enableLeftMotor() {
		pinMode(LEFT_MOTOR_PIN, 1);
	}
	
	static void enableRightMotor() {
		pinMode(RIGHT_MOTOR_PIN, 1);
	}
	
}

class CameraDriver {
	
	// srututututtu
	
}

2.warstwa między sprzętem, a warstwą obsługi poleceń

class MotorController {

	static void goForward() {
		MotorDriver::enableLeftMotor();
		MotorDriver::enableRightMotor();
	}

}

class CameraController {
	
	static void takePicture() {
		CameraDriver::picturePrepare();
		
		char* picture;
		uint32 pictureSize;
		
		CameraDriver::takePicture(picture, pictureSize);
		
		Output::writeRaw(picture, pictureSize);
	}
		
}

1.warstwa obsługi poleceń

class CommandManager {

	char* commandBuffer;

	static void onCommandReceived() {
		if (strcmp(this->commandBuffer, 'motor.go_forward')) {
			MotorController::goForward();
		}
		
		if (strcmp(this->commandBuffer, 'camera.take_picture')) {
			CameraController::takePicture();
		}
	}

}

No i klasycznie: każda z warstw odnosi się tylko do tej bezpośrednio nad nią (tj. pierwsza do drugiej, druga do trzeciej), czyli warstwa obsługi poleceń nie powinna ani razu komunikować się bezpośrednio ze sprzętem, od tego jest ta pośrednicząca. Imho daje to bardzo jasny obraz tego, co się dzieje w kodzie oraz jednocześnie jest łatwo rozszerzalne.

Teoretycznie wpychanie wszędzie singletonów to bardzo złe rozwiązanie - nie wiem natomiast, ile masz wolnej pamięci na tym swoim mikrokontrolerze :P

0

O pamięć nie ma co się martwić. O warstwie obsługi poleceń myślałem jako kontrolerze. A describer miałby być tą warstwą pośrednią. Chociaż muszę przyznać Ci rację. Można było by dodać coś między kontrolerem a describerem, coś w stylu api co ułatwiło by oprogramowanie pilotów. A co do singletona to chciałem tak zrobić ponieważ:
Piszę szybką zmianę w np. module kamery, jakaś mała pierdoła. Niestety wcisnął mi się przez przypadek klawisz przez co po zapisie robot cały czas jeździ a ja na szybko szukam błędu. Poza tym mogą być tam różne urządzenia przez co klasa która nie będzie singletonem może się bardzo rozrosnąć.

0

O pamięć nie ma co się martwić
Zatem optowałbym za pełnoprawnym, kobylastym rozwiązaniem:

class MotorDriver {
	
protected:
	uint8 leftMotorPin;
	uint8 rightMotorPin;
 
public:
	MotorDriver(uint8 leftMotorPin, uint8 rightMotorPin) : leftMotorPin(leftMotorPin), rightMotorPin(rightMotorPin) {
	}

    void enableLeftMotor() {
        pinMode(this->leftMotorPin, 1);
    }
 
    void enableRightMotor() {
        pinMode(this->rightMotorPin, 1);
    }
 
}
 
class CameraDriver {
 
    // srututututtu
 
}
typedef void (*ControllerAction)(Command);
typedef std::map<std::string, ControllerAction> ControllerActionsMap;

class AbstractController {
	
protected:
	ControllerActionsMap controllerActions;
	
	void registerCommand(string commandName, ControllerAction controllerAction) {
		this->controllerActions[commandName] = controllerAction;
	}
	
public:
	bool handleCommand(Command command) {
		// sprawdź czy dana akcja znajduje się na naszej liście obsługiwanych akcji
		ControllerActionsMap::iterator fnc = this->controllerActions.find(command.commandName);
		
		// jeśli nie - zwróć false i na tym poprzestań
		if (fnc == this->controllerActions.end()) {
			return false;
		}
		
		// jeśli zaś tak - przeskocz do tej metody i zwróć sukces, jako że akcja została obsłużona
		(this.*(fnc->second))();
		return true;
	}
	
}

class MotorController {
	
protected:
	MotorDriver* motorDriver;
	
public:
	MotorController(MotorDriver* motorDriver) : motorDriver(motorDriver) {
		this.registerCommand("motor.go_forward", &MotorController::cmdGoForward);
	}

    void cmdGoForward(Command command) {
        this->motorDriver->enableLeftMotor();
        this->motorDriver->enableRightMotor();
    }
 
}
 
class CameraController {
	
protected:
	OutputDriver* outputDriver;
	CameraDriver* cameraDriver;

public:
	CameraController(OutputDriver* outputDriver, CameraDriver* cameraDriver) : outputDriver(outputDriver), cameraDriver(cameraDriver) {
		this.registerCommand("camera.take_picture", &CameraController::cmdTakePicture);
	}
 
    void cmdTakePicture(Command command) {
        this->cameraDriver->picturePrepare();
 
        char* picture;
        uint32 pictureSize;
 
        this->cameraDriver->takePicture(picture, pictureSize);
        this->outputDriver->writeRaw(picture, pictureSize);
		
		this->cameraDriver->releasePicture(picture, pictureSize);
    }
 
}
struct Command {
	
	string commandName;
	vector<string> commandArgs;
	
}

class CommandManager {
 
protected:
    string commandBuffer;
	vector<AbstractController*> controllers;
	
	Command parseCommandBuffer() {
		Command command = new Command();
		
		// uwaga, magia
		
		return command;
	}
 
public:
	void registerController(AbstractController* controller) {
		this->controllers.push_back(controller);
	}

    void dispatchCommand() {
		Command command = this->parseCommandBuffer();

		bool commandHandled = false;
		
		for (AbstractController &controller: this->controllers) {
			commandHandled |= controller->handleCommand(command);
		}
		
		if (!commandHandled) {
			// wyświetl błąd czy coś
		}

		free(command);
		this->commandBuffer.clear();
    }
 
}
const uint8
	LEFT_MOTOR_PIN = 9,
	RIGHT_MOTOR_PIN = 10;

int main() {
	MotorDriver* motorDriver = new MotorDriver(LEFT_MOTOR_PIN, RIGHT_MOTOR_PIN);
	OutputDriver* outputDriver = new OutputDriver();
	CameraDriver* cameraDriver = new CameraDriver();
	
	MotorController* motorController = new MotorController(MotorDriver);
	CameraController* cameraController = new CameraController(OutputDriver, CameraDriver);
	
	CommandManager* commandManager = new CommandManager();
	commandManager->registerController(motorController);
	commandManager->registerController(cameraController);
	
	Serial serial;
	
	while (true) {
		serial.readcośtamcośtam();
		commandManager->cośtamcośtam();
	}
}

uwaga: kod pisany na kolanie

Wykorzystując wzorzec DI, ładnie wydzielone zostały odpowiedzialności (przede wszystkim dodanie nowej komendy wymaga już jedynie zmiany danego kontrolera, a nie grzebania wewnątrz CommandManagera) oraz odpowiednie stałe, wszystko tworzone jest na początku życia aplikacji, dlatego też bezproblemowo kontrolowalne ;-)

Niestety wcisnął mi się przez przypadek klawisz przez co po zapisie robot cały czas jeździ a ja na szybko szukam błędu
???

klasa która nie będzie singletonem może się bardzo rozrosnąć.
???

1 użytkowników online, w tym zalogowanych: 0, gości: 1