This is a project about a domotics control system based on arduino. Nothing great; it's only for testing myself and making stuff.
Before I started coding this, I searched on the net for some useful tips to use in my code, but I still remain a novice in programming.
The project is about 1600 lines long. I'm obligated to load it on an external site here.
DomoS.h
#ifndef DomoS_H
#define DomoS_H
#include <arduino.h>
class DomoS
{
private:
static const byte STRINGMAXLEN = 64; //Maximum length for the command string
static const byte SUBSTRINGMAXLEN = 16; //Maximum length for a single command
static const byte MAXADDRESSPIN = 8; //Maximum number of adressing pin
static const byte MAXNAMELEN = 10; //Maximum length for a peripheral name
static const byte FILEVER = 0; //The version of the file type
/*
The DomoS setting file is made of two parts
1) The header of the file which contains all of the configuration parameters
of the DomoS module and the version of the file type
2) The body of the file which contains all the settings for the different
peripherals [for a explanation go to the declaration of the type]
These are stored in sequential order
With version 0 the different arduino EEPROM can contain up to:
ATmega168 and ATmega8 [512byte]: 45 peripherals
ATmega328 [1024byte]: 91 peripherals
ATmega1280 and ATmega2560 [4096byte]: 371 peripherals
*/
struct DomoSFileHeader //size 13byte
{
//The order here is also the order in the EEPROM
//RESPECT THIS ORDER
//The "EEPROM #" on the right of the variable name is the standard position in the EEPROM
//Version of the file type, in case of change through developement
byte fileVer; //EEPROM 2
//Copy of the configuration parameters
byte numPeripheral; //EEPROM 3
byte numAddressPin; //EEPROM 4
byte addressPin[MAXADDRESSPIN]; //EEPROM 5-12
byte outputPin; //EEPROM 13
byte writeToEeprom; //EEPROM 14
};
struct DomoSFileBody //size 11byte
{
char name[MAXNAMELEN]; //The name of the peripheral
byte number; //The number of the peripheral and the addressing parameter
};
//Setup function
void GetConfigurationDataFromEeprom(); //Gets the configurations data from the EEPROM
void FirstStart(); //Starts all the magic before the first start
boolean CheckSetupData(); //Checks if the system has been already setupped
void ClearEeprom(); //Clears all the EEPROM
void WriteSetupData(); //Writes to the first two cells of EEPROM the setup check values
void AskData(DomoSFileHeader & data); //Asks to the user the configuration parameters
void WriteConfigurationDataToEeprom (DomoSFileHeader data); //Writes the data variables into the EEPROM
void Initialize(); //Initializes the DomoS module
void UpdateNumPeripheral(char type); //Updates the peripheral number
boolean ConvertDecimalToBinary(int number, boolean result[]); //Converts a decimal number to an array of boolean, return false if the number is greater than what the module can handle, else true
void SetAddressing(boolean addressing[]); //Sets up the addressing lines
byte SeparateCommandBySpace(); //Separates the _command string into two strings, the first is the first word before the space, the second is the original string with the first word deleted, returns the number of char written in _subCommand
byte CompareSubCommand(); //Compares a command with the commands' dictionary
boolean FetchCommand(); //Fetches a command from the serial port
void DoCommand(byte numCommand); //Executes a command
byte GetCommand(char* command); //Gets the number of a command
void CommandToLowerCase(); //Converts the _command string to lower case
void SubCommandShiftLeft(); //Shifts left of one position all the character in the _subCommand string
byte SearchPeripheralByName(char peripheral[]); //Searches the peripheral by name
void GetPeripheralName(byte numPeripheral, char name[]); //Writes in char name[] the name of numPeripheral-th peripheral
boolean SearchDuplicatedPeripheral(DomoSFileBody & peripheral, boolean nameCustom, boolean numberCustom); //Checks if the peripheral is unique else tries to make it unique
void GetPeripheralNumber(byte numPeripheral, byte & number); //Writes in "number" the number of numPeripheral-th peripheral
byte ConvertBinaryStringToDecimal();
void BlankNewPeripheral(DomoSFileBody & peripheral);
boolean CreateParameterCheck(DomoSFileBody & peripheral);
boolean WritePeripheral(DomoSFileBody peripheral, byte position);
byte SearchPeripheralByNumber(byte number);
void ComposeStringPeripheral(DomoSFileBody peripheral, byte phrase);
byte GetError();
boolean WritePeripheralToEeprom(DomoSFileBody peripheral, byte position); //Writes the peripheral to the EEPROM
//boolean WritePeripheralToSd(DomoSFileBody peripheral, byte position); //Function not yet developed
void Create(); //Create a peripheral
void Turn(); //Activate a peripheral
void Delete(); //Delete a peripheral, probably this wont be developed
void Exit(); //Turn off DomoS module
void Reset(); //Resets the DomoS module
void List(); //Give a list of all the installed peripheral
int parseInt();
void ThrownError();
//Declaration of configuration variables
byte _numAddressPin; //Number of pins used for addressing peripherals
byte _addressPin[MAXADDRESSPIN]; //Pins used for addressing
byte _outputPin; //The pin used for the output, pin 6 will automatically be selected
byte _writeToEeprom; //-1 if DomoS must store the peripheral settings in the EEPROM, else the CSPin where the SD card is connected for storing the settings in DomoS.dat file
byte _numPeripheral; //Number of peripheral created by user
byte _fileVer; //The version of the file type
boolean _on; //Tell if DOMOS system is on
byte _lastError; //Tell the last error thrown by DomoS
/*
Declaration of strings constant
Inizialization in DomoS.cpp
*/
static const byte NCOMMAND = 8; //number of commands allowed
static const char* COMMAND[NCOMMAND]; //Array of commands, for explanation go to inizialization
static const int NPHRASE = 12; //Number of phrases, for eventually translation
static const char* PHRASE[NPHRASE];
static const int NERROR = 24;
static const char* ERROR[NERROR];
static const int START = 2;
static const byte SETUP[START]; //These two values are stored in the first two cell of EEPROM, if already present the system have been already setup, if not launch the first start procedure
static const int RCLOAD = 300; //Number of millisecond necessary for charging of RC circuit
//String for fetching and checking commands
char _command[STRINGMAXLEN];
char _subCommand[SUBSTRINGMAXLEN];
public:
//Constructor
DomoS();
void Work();
boolean IsOn(); //Control if the module is ready to handle new request
//Errors constant
static const byte OK = 0;
static const byte COMMANDSTRINGTOOLONG = 1;
static const byte SUBCOMMANDSTRINGTOOLONG = 2;
static const byte COMMANDNOTRECOGNIZED = 3;
static const byte NOCOMMANDPARAMETERS = 4;
static const byte SUBCOMMANDNOTRECOGNIZED = 5;
static const byte NAMENOTDEFINED = 6;
static const byte NAMETOOLONG = 7;
static const byte ASNOTDEFINED = 8;
static const byte BINARYNUMBERTOOLONG = 9;
static const byte EEPROMISFULL = 10;
static const byte PERIPHERALNOTFOUND = 11;
static const byte STRANGETURNPARAMETER = 12;
static const byte BADTHINGSHAPPEN = 13;
static const byte PERIPHERALZERONOTALLOWED = 14;
static const byte PERIPHERALMAXIMUMNUMBERREACH = 15;
static const byte PERIPHERALNAMENOTUNIQUE = 16;
static const byte PERIPHERALNUMBERNOTUNIQUE = 17;
static const byte CANTFINDFREENAME = 18;
static const byte CANTFINDFREENUMBER = 19;
static const byte VOLTAGETOOLOW = 20;
static const byte VOLTAGETOOHIGH = 21;
static const byte DECIMALNUMBERTOOBIG = 22;
static const byte THEREAREZEROPERIPHERAL = 23;
};
#endif
DomoS.cpp
-
1\$\begingroup\$ You are very unlikely to get a good review unless you pick out the relevant parts you want reviewed and add them to the post. \$\endgroup\$Lstor– Lstor2013年07月22日 11:12:06 +00:00Commented Jul 22, 2013 at 11:12
1 Answer 1
I am not familiar with Arduino, but as a general C++ developer, I've noticed the following issues (looking only at the code in the question, not the one behind links):
No const-correctness.
The biggest problem in the code, one which would cause me to return it back to the developer if I were reviewing this at work.
None of your member functions are classified as
const
, meaning they will not be callable on aconst
object of typeDomoS
, or such an object accessed through aconst
-qualified path (such as aconst DomoS &
).Even more importantly, your functions which take a pointer or reference parameter do not use
const
on the destination type either, which again meansconst
objects or, even worse, temporaries cannot be used as arguments for those parameters. This way, you also deprive yourself of a great opportunity for self-documenting code, creating space for confusion of the user (or future maintainer) of the class. Example:byte GetCommand(char* command); //Gets the number of a command
Based on its signature, this function:
- returns a
byte
. Given the comment, I expect this to be a number of thecommand
- can write something into the string pointed to by
command
. As a user, I am confused. Will it do so? What if the buffer I pass in is not large enough for what the function will write? How do I determine what "large enough" is? Why can't I use a string literal as the argument (string literals are of typeconst char[]
, and thus passing one here would cast away constness)? - can modify the
DomoS
object on which it's invoked. This is definitely not something I would expect from a function whose name starts withGet
.
Instead, I would expect the function's signature to be:
byte GetCommand(const char* command) const; //Returns the number of a command
The same applies to many other functions in the class, of course. This was just an example.
Writing const-correct code also prevents some programmer errors, such as accidentally modifying an input-only parameter.
Last but not least, it is a real pain to integrate a non-const-correct module into otherwise const-correct code. You have to use
const_cast
s everywhere, which is 1) tedious to write, 2) a great source of bugs and 3) a maintenance nightmare. I understand this is less of an issue here since almost all of the class's functions areprivate
, but it's a general principle worth mentioning anyway. From the external interface at least,IsOn()
should definitely beconst
.A quick summary of "const-correctness":
- A member function which does not modify the observable state of the object it's invoked on should be marked as
const
, so that it can be called onconst
-qualified objects. - A function taking a parameter by reference and not modifying the parameter should take the parameter by
const &
. This is particularly important because it allows temporary objects to be bound to the reference (temporaries can bind toconst
-references, but not to non-const
references). - A function taking a parameter by pointer and not modifying the object pointed to should declare the parameter as pointer to
const
.
Imagine this code:
void prettyPrint(const std::string &s) { std::cout << s.size() << ' ' << s << '\n'; } std::string x = "Hello"; std::string y = " World!"; prettyPrint(x + y);
If there was no
const
in the signature ofprint()
, this wouldn't compile, asx + y
returns a temporary. If the purely observing member functionsize()
ofstd::string
wasn't marked asconst
, we couldn't calls.size()
insideprettyPrint()
becauses
is aconst
-reference.- returns a
Taking structures by value.
Some of your functions take parameters of type
DomoSFileBody
by value. This can be OK if the function does any internal modification on the arguments passed in: copying at call site can be more efficient than copying internally, as discussed in this great article.However, if the function does not modify its argument, you can save a significant amount of copying by passing these arguments as
const DomoSFileBody &
instead.Hand-crafted enumeration for the error code.
The error constants would be perfectly represented as an enumeration. This has a distinct advantage of creating a full-fledged type, so that overload resolution can be used and code is more self-documenting. Function signature
byte GetError();
doesn't really tell me much, butErrorCode GetError();
clearly says what values I can expect.In C++11 (and many compilers support this even before that), you can explicitly specify the underlying type for an enumeration, so it could be a
byte
even so. The definition would then look like this://Errors constant enum ErrorCode { OK, COMMANDSTRINGTOOLONG, SUBCOMMANDSTRINGTOOLONG, COMMANDNOTRECOGNIZED, NOCOMMANDPARAMETERS, SUBCOMMANDNOTRECOGNIZED, NAMENOTDEFINED, NAMETOOLONG, ASNOTDEFINED, BINARYNUMBERTOOLONG, EEPROMISFULL, PERIPHERALNOTFOUND, STRANGETURNPARAMETER, BADTHINGSHAPPEN, PERIPHERALZERONOTALLOWED, PERIPHERALMAXIMUMNUMBERREACH, PERIPHERALNAMENOTUNIQUE, PERIPHERALNUMBERNOTUNIQUE, CANTFINDFREENAME, CANTFINDFREENUMBER, VOLTAGETOOLOW, VOLTAGETOOHIGH, DECIMALNUMBERTOOBIG, THEREAREZEROPERIPHERAL, NERROR };
Note the inclusion of
NERROR
as the last enumerator. This has the advantage of keeping this value always1 + last enumerator
, so that you cannot forget to increase the value if you later need to add further error codes.I would also introduce underscores to separate words in the enumerators (e.g.
COMMAND_STRING_TOO_LONG
), but that is mostly a matter of personal preference.Using signed types for sizes.
NPHRASE
andSTART
(and alsoNERROR
, but see above for that) are defined with typeconst int
, even though they represent non-negative quantities (a number of something). The ideal type for this isstd::size_t
, which is again self-documenting as "this variable holds a size or count."If you cannot use this for reasons of type size (i.e. it's too big), at least use
const unsigned int
for the constants. That way, you clearly document they store non-negative quantities. It also prevents potential signed/unsigned issues when interacting with the standard library, which correctly uses unsigned types (std::size_t
, actually) to represent sizes and counts.Inconsistent usage of
char* p
andchar p[]
in function arguments.These are semantically equivalent, but you should preferably pick one and stick to it.
-
\$\begingroup\$ 0)Someone modified the tag of the question from c to c++, in fact the code use only c construct beside of the main class 1)Is the first time i hear the word "const correctnes", i took a look at wikipedia but i didn't understand well what you mean and how should i use this 2)Thanks for the tip, i'll use this in my next works 3)the arduino compiler don't support enumerator, i tried to implement it but without success \$\endgroup\$user92608– user926082013年10月11日 16:56:30 +00:00Commented Oct 11, 2013 at 16:56
-
\$\begingroup\$ @user92608 So? The class is C++ (class, references etc.). And pretty much all the points are valid for C code as well. \$\endgroup\$Angew is no longer proud of SO– Angew is no longer proud of SO2013年10月11日 16:59:45 +00:00Commented Oct 11, 2013 at 16:59
-
\$\begingroup\$ 4)Yes, you're right, that should only be a typo, these should also be byte const [already unsigned] 5)Luckily only one function use the char* p style, maybe another typo \$\endgroup\$user92608– user926082013年10月11日 17:08:09 +00:00Commented Oct 11, 2013 at 17:08
-
\$\begingroup\$ @user92608 I've elaborated a bit more on const-correctness. If enumerations are not supported by the compiler, then your approach is the best that can be done. You might at least introduce a
typedef
for the error code type. While it will not be type-safe, it will at least be self-documenting. \$\endgroup\$Angew is no longer proud of SO– Angew is no longer proud of SO2013年10月11日 17:28:18 +00:00Commented Oct 11, 2013 at 17:28 -
\$\begingroup\$ Then, if i understood well i should write boolean CheckSetupData(); -> const boolean CheckSetupData(); because this function don't modify the internal state of the class. But this function byte SeparateCommandBySpace(); can't be const because it modify some internal variables. Instead in this function void WriteConfigurationDataToEeprom (DomoSFileHeader data); i should write (const DomoSFileHeader & data) for avoid the copying of the structure Have I understood well?? \$\endgroup\$user92608– user926082013年10月12日 15:20:59 +00:00Commented Oct 12, 2013 at 15:20