Much of this is "down in the weeds" of the software but that's where I live at present. File under "Play with Bittle X."
If you're not interested in software details, you can skim/skip much of this. There are a few tests you might find useful.
Questions I want to answer are:
Why does the 'B' command only control the boot melody?
What does the 'b' command control?
My understanding of this command behavior has changed 3 times during the analysis of the code. It didn't help that I haven't been able to correlate the running code with any github code.
There are 2 commands to control sound through the serial connection: one controls volume and the other controls mute. Both commands support playing a tune with a list of note specifications. A note specification is a pair of numbers: the first is the tone to play and the second is the time duration to play the tone. Each command uses a unique ASCII character prefix that identifies the command, The volume command prefix is 'b' and the mute command prefix is 'B'. The command prefix is case sensitive.
Volume Control
First, I note a difference between the volume command ('b') specified on the reference page and the other pages and code comments that identify another form for the volume command
The reference page says
token ASCII Binary Function and Format
T_BEEP 'b' b note1 duration1 note2 duration2 ... e.g. b12 8 14 8 16 8 17 8 19 4
T_BEEP_BIN 'B' B note1 duration1 note2 duration2 ... e.g. B12 8 14 8 16 8 17 8 19 4
A single 'B' will toggle the melody on/off
Only the binary command ('B') has a single character format to control playing of a melody. (i.e., any melody not just the boot melody)
The other pages identify a two additional forms of the volume command ('b'):
a single character form to toggle the volume on and off (the same toggling mute)
volume level form ('b'Volume)
The code1 in OpenCat.h in the OpenCat32 codebase has
#define T_BEEP 'b' //b note1 duration1 note2 duration2 ... e.g. b12 8 14 8 16 8 17 8 19 4 \
//bVolume will change the volume of the sound, in scale of 0~10. 0 will
mute all sound effect. e.g. b3. \
//a single 'b' will toggle all sound on/off
#define T_BEEP_BIN 'B' //B note1 duration1 note2 duration2 ... e.g. B12 8 14 8 16 8 17 8 19 4 \
//a single 'B' will toggle all sound on/off
The comment has the volume level form (bVolume) to control the volume of the sound. It also adds a third method of muting the sound by specifying that 'b0' will mute all sound.
Note that the first form on the reference page,
b note1 duration1 note2 duration2
appears to be contradicted in the first example,
b12 8 14 8 16 8 17 8 19 4
The form "b12" is an invalid form of bVolume according to the volume range 0-10 specified in the following comment. The tone list now has a trailing note value, 4, as a result.
Unless is a typo and is missing a space and it should read
b 12 8 14 8 16 8 17 8 19 4
This specifies 5 notes. Turns out, however, it's not a typo.
The bVolume form is the form of interest. The comment says the form may be used to control the volume of the sound (i.e., the following note list), e.g.,
b3 12 8 14 8 16 8 17 8 19 4
or by itself without a note list, e.g.,
b3
to set a global volume for all sounds. Turns out, this is an incorrect reading.
There are 3 forms for the volume command:
Toggle sound (single 'b')
Volume specification ("bVolume" where Volume is in the range [0-10])
Each form is a complete input line. Specifically, there is no form that specifies the volume of a sound specification, e.g., b3 12 8 14 8 16 8 17 8 19 4. Strangely, if you issue that command you will get equally strange results.
Finally, on the User Manual page, we are told that
"b0" will mute the buzzer, while "b" will toggle mute/unmute. If the volume is set to 0 and muted, it will be set to 5 when unmuted.
The test
send the toggle mute command :('b')
send melody command ('o1') and the melody plays.
send the toggle mute command :('b')
send melody command ('o1') and the melody still plays
checks the mute/unmute behavior. One of the above 'b' commands should have muted the melody.
The trailing backslash on the comments is superfluous and generates a warning in the compiler
Code Review
Questions to I want to answer are:
Why does the 'B' command only control the boot melody?
What does the 'b' command control?
Why does the 'B' command only control the boot melody?
The test for this behavior is
Power on the robot and note if the boot melody plays
Connect the robot to the serial console
Send the command 'B'. The console should display a message giving the mute status: "Mute" or "Unmute"
Send the 'B' command until the "Mute" status is shown.
Disconnect the USB serial cable and power the robot off.
Power the robot on and the boot melody should not play.
Connect the robot to the serial console.
Send the command 'm1' and the melody should play.
Repeat steps 1-3.
Send the 'B' command until the "Unmute" status is shown.
Disconnect the USB serial cable and power the robot off.
Power the robot on and the boot melody should play.
newBoardQ compares the EEPROM birthmark value with a standard value and returns the boolean result of a mismatch.
Finally, playMelody uses the value of soundState to control playing the melody.
void playMelody(byte m[], int len) {
if (soundState) {
// play the melody...
}
}
Thus the boot melody is controlled by two variables: newBoard and soundState.
We can quickly eliminate newBoard from further consideration by noting that the value it is based upon, the EEPROM birthmark value, is unconditionally updated to the standard value during the call to initRobot and after the call to i2cEepromSetup
void initRobot() {
//...
i2cEepromSetup();
//...
i2c_eeprom_write_byte(EEPROM_BIRTHMARK_ADDRESS, BIRTHMARK); // finish the test and mark the board as initialized
//...
}
Thus it should only happen once or as a result of the reset command ('!') which overwrites it.
void reaction() {
//...
switch (token) {
//...
case T_RESET:
{
//mark the board as uninitialized
i2c_eeprom_write_byte(EEPROM_BIRTHMARK_ADDRESS, 'R');
//...
break;
}
//...
}
//...
}
We now look at soundState and how the 'B' command alters it.
The 'B' command simply inverts the value of EEPROM_BOOTUP_SOUND_STATE and rewrites it. Thus, on the next reboot the initRobot function reads the value from EEPROM before calling i2cEepromSetup (which plays the melody)
Unfortunately, this isn't the only place the EEPROM_BOOTUP_SOUND_STATE value is rewritten nor the only place that soundState is updated. We can ignore the updates to the EEPROM_BOOTUP_SOUND_STATE because this doesn't match the problem symptom i.e., the 'B' command does turn the boot melody off. The problem is the melodies continue to play after the 'B' command. The melody command is fairly basic
There are only 4 places where soundState is updated
initRobot
Twice in the T_BEEP command handler
T_BEEP_BIN command handler
The test of 'B' command does not invoke the other commands so they should have no effect on soundState.
So it appears that the 'B' command only affects the boot melody. One of these 'B' commands should turn the melody sound off.
send the toggle mute command :('B')
send melody command ('o1') and the melody plays.
send the toggle mute command :('B')
send melody command ('o1') and the melody still plays
What does the 'b' command control?
The 'b' command is the ASCII command to control mute, volume, and play notes. The example from the reference page shows how to play notes:
b 12 8 14 8 16 8 17 8 19 4
The volume of the note is not set by appending a volume value to the command prefix
b2 12 8 14 8 16 8 17 8 19 4
This produces a strange sound.
The single volume setting command is
b2
Issuing this command has no effect I can decern if I then playing a melody
b2
m1
The last form toggles the mute state
b
I note that the code I have says that toggling the mute and unmute prints the "Unmute/Mute" via the printToAllPorts function but that isn't the observed behavior.
The volume command is handled in the reaction function
void reaction() {
//...
switch (token) {
//...
case T_BEEP: //beep(tone, duration): tone 0 is pause, duration range is 0~255
if (token == T_INDEXED_SIMULTANEOUS_ASC && cmdLen == 0)
//...
else {
if (token == T_CALIBRATE) {
//...
} else if (token == T_BEEP) {
if (inLen == 0) { // toggle on/off the bootup melody
//...
} else if (inLen == 1) { // change the buzzer's volume
//...
} else if (target[1] > 0) {
beep(target[0], 1000 / target[1]);
}
}
//...
}
//...
}
//...
}
This case of interest is the second one, the bVolume form
The 'b' command echos the mute state to the serial port.
void reaction() {
//...
switch (token) {
//...
case T_BEEP: //beep(tone, duration): tone 0 is pause, duration range is 0~255
if (token == T_INDEXED_SIMULTANEOUS_ASC && cmdLen == 0)
//...
else {
if (token == T_CALIBRATE) {
//...
} else if (token == T_BEEP) {
if (inLen == 0) { // toggle on/off the bootup melody
//...
} else if (inLen == 1) { //change the buzzer's volume
buzzerVolume = max(0, min(10, target[0])); //in scale of 0~10
if (soundState ^ (buzzerVolume > 0)) //only print if the soundState changes
printToAllPorts(buzzerVolume ? "Unmute" : "Muted");
soundState = buzzerVolume;
i2c_eeprom_write_byte(EEPROM_BOOTUP_SOUND_STATE, soundState);
i2c_eeprom_write_byte(EEPROM_BUZZER_VOLUME, buzzerVolume);
PTF("Changing volume to ");
PT(buzzerVolume);
PTL("/10");
playMelody(volumeTest, sizeof(volumeTest) / 2);
} else if (target[1] > 0) {
//...
}
}
//...
So the variable buzzerVolume is what controls the volume. The beep function passes the (reduced) variable to the tone function
void beep(float note, float duration = 50, int pause = 0, byte repeat = 1) {
if (soundState) {
for (byte r = 0; r < repeat; r++) {
if (note > 0) {
tone(BUZZER
, BASE_PITCH * pow(1.05946, note)
, duration
, buzzerVolume / amplifierFactor
);
}
else {
//...
}
//...
}
}
}
The tone function passes the value down to the PWM code which use the value to modify the PWM signal, I'm not going into that code now other than to say that it has discernable effect to my ear. This may be due to the speaker type on my robot. I believe it's passive so it doesn't perform the amplification.
I'm not fully satisfied with the analysis above but it has prompted me to get the opencat_serial program running to make the testing easier.
Preface
Much of this is "down in the weeds" of the software but that's where I live at present. File under "Play with Bittle X."
If you're not interested in software details, you can skim/skip much of this. There are a few tests you might find useful.
Questions I want to answer are:
Why does the 'B' command only control the boot melody?
What does the 'b' command control?
My understanding of this command behavior has changed 3 times during the analysis of the code. It didn't help that I haven't been able to correlate the running code with any github code.
An Evil Space
Here are 3 links that reference sound
Reference page: Serial Protocol - Petoi Doc Center
Mobile App: Controller - Petoi Doc Center
Bittle X user Manual: 2 - Open the Box - Bittle X User Manual (petoi.com)
In addition to these is the actual code.
Analysis
There are 2 commands to control sound through the serial connection: one controls volume and the other controls mute. Both commands support playing a tune with a list of note specifications. A note specification is a pair of numbers: the first is the tone to play and the second is the time duration to play the tone. Each command uses a unique ASCII character prefix that identifies the command, The volume command prefix is 'b' and the mute command prefix is 'B'. The command prefix is case sensitive.
Volume Control
First, I note a difference between the volume command ('b') specified on the reference page and the other pages and code comments that identify another form for the volume command
The reference page says
token ASCII Binary Function and Format
T_BEEP 'b' b note1 duration1 note2 duration2 ... e.g. b12 8 14 8 16 8 17 8 19 4
T_BEEP_BIN 'B' B note1 duration1 note2 duration2 ... e.g. B12 8 14 8 16 8 17 8 19 4
A single 'B' will toggle the melody on/off
Only the binary command ('B') has a single character format to control playing of a melody. (i.e., any melody not just the boot melody)
The other pages identify a two additional forms of the volume command ('b'):
a single character form to toggle the volume on and off (the same toggling mute)
volume level form ('b'Volume)
The code1 in OpenCat.h in the OpenCat32 codebase has
#define T_BEEP 'b' //b note1 duration1 note2 duration2 ... e.g. b12 8 14 8 16 8 17 8 19 4 \ //bVolume will change the volume of the sound, in scale of 0~10. 0 will mute all sound effect. e.g. b3. \ //a single 'b' will toggle all sound on/off #define T_BEEP_BIN 'B' //B note1 duration1 note2 duration2 ... e.g. B12 8 14 8 16 8 17 8 19 4 \ //a single 'B' will toggle all sound on/off
The comment has the volume level form (bVolume) to control the volume of the sound. It also adds a third method of muting the sound by specifying that 'b0' will mute all sound.
Note that the first form on the reference page,
b note1 duration1 note2 duration2
appears to be contradicted in the first example,
b12 8 14 8 16 8 17 8 19 4
The form "b12" is an invalid form of bVolume according to the volume range 0-10 specified in the following comment. The tone list now has a trailing note value, 4, as a result.
Unless is a typo and is missing a space and it should read
b 12 8 14 8 16 8 17 8 19 4
This specifies 5 notes. Turns out, however, it's not a typo.
The bVolume form is the form of interest. The comment says the form may be used to control the volume of the sound (i.e., the following note list), e.g.,
b3 12 8 14 8 16 8 17 8 19 4
or by itself without a note list, e.g.,
b3
to set a global volume for all sounds. Turns out, this is an incorrect reading.
There are 3 forms for the volume command:
Toggle sound (single 'b')
Volume specification ("bVolume" where Volume is in the range [0-10])
Sound specification ('b' note duration note duration...)
Each form is a complete input line. Specifically, there is no form that specifies the volume of a sound specification, e.g., b3 12 8 14 8 16 8 17 8 19 4. Strangely, if you issue that command you will get equally strange results.
Finally, on the User Manual page, we are told that
The test
send the toggle mute command :('b')
send melody command ('o1') and the melody plays.
send the toggle mute command :('b')
send melody command ('o1') and the melody still plays
checks the mute/unmute behavior. One of the above 'b' commands should have muted the melody.
The trailing backslash on the comments is superfluous and generates a warning in the compiler
Code Review
Questions to I want to answer are:
Why does the 'B' command only control the boot melody?
What does the 'b' command control?
Why does the 'B' command only control the boot melody?
The test for this behavior is
Power on the robot and note if the boot melody plays
Connect the robot to the serial console
Send the command 'B'. The console should display a message giving the mute status: "Mute" or "Unmute"
Send the 'B' command until the "Mute" status is shown.
Disconnect the USB serial cable and power the robot off.
Power the robot on and the boot melody should not play.
Connect the robot to the serial console.
Send the command 'm1' and the melody should play.
Repeat steps 1-3.
Send the 'B' command until the "Unmute" status is shown.
Disconnect the USB serial cable and power the robot off.
Power the robot on and the boot melody should play.
The boot melody is defined sound.h
byte melodyNormalBoot[] = { 8, 13, 10, 13, 8, 5, 8, 3, 5, 8, //tone 4, 4, 8, 8, 4, 8, 8, 8, 8, 4, //relative duration, 8 means 1/8 note length };
The setup function calls the initRobot function
void setup() { //... initRobot(); }
The initRobot function calls the i2cEepromSetup function
void initRobot() { //... i2cEepromSetup(); //... }
i2cEepromSetup conditionally calls playMelody for the boot melody according to the return value of newBoardQ
void i2cEepromSetup() { newBoard = newBoardQ(EEPROM_BIRTHMARK_ADDRESS); if (newBoard) { //... } else playMelody(melodyNormalBoot, sizeof(melodyNormalBoot) / 2); }
newBoardQ compares the EEPROM birthmark value with a standard value and returns the boolean result of a mismatch.
Finally, playMelody uses the value of soundState to control playing the melody.
void playMelody(byte m[], int len) { if (soundState) { // play the melody... } }
Thus the boot melody is controlled by two variables: newBoard and soundState.
We can quickly eliminate newBoard from further consideration by noting that the value it is based upon, the EEPROM birthmark value, is unconditionally updated to the standard value during the call to initRobot and after the call to i2cEepromSetup
void initRobot() { //... i2cEepromSetup(); //... i2c_eeprom_write_byte(EEPROM_BIRTHMARK_ADDRESS, BIRTHMARK); // finish the test and mark the board as initialized //... }
Thus it should only happen once or as a result of the reset command ('!') which overwrites it.
void reaction() { //... switch (token) { //... case T_RESET: { //mark the board as uninitialized i2c_eeprom_write_byte(EEPROM_BIRTHMARK_ADDRESS, 'R'); //... break; } //... } //... }
We now look at soundState and how the 'B' command alters it.
void reaction() { //... switch (token) { //... case T_BEEP_BIN: { if (cmdLen == 0) { // toggle on/off the bootup melody soundState = !i2c_eeprom_read_byte(EEPROM_BOOTUP_SOUND_STATE); printToAllPorts(soundState ? "Unmute" : "Muted"); i2c_eeprom_write_byte(EEPROM_BOOTUP_SOUND_STATE, soundState); } else { //... } break; } //... } //... }
The 'B' command simply inverts the value of EEPROM_BOOTUP_SOUND_STATE and rewrites it. Thus, on the next reboot the initRobot function reads the value from EEPROM before calling i2cEepromSetup (which plays the melody)
void initRobot() { //... soundState = i2c_eeprom_read_byte(EEPROM_BOOTUP_SOUND_STATE); //... i2cEepromSetup(); //... }
Unfortunately, this isn't the only place the EEPROM_BOOTUP_SOUND_STATE value is rewritten nor the only place that soundState is updated. We can ignore the updates to the EEPROM_BOOTUP_SOUND_STATE because this doesn't match the problem symptom i.e., the 'B' command does turn the boot melody off. The problem is the melodies continue to play after the 'B' command. The melody command is fairly basic
void reaction() { //... switch (token) { //... case T_MELODY: { playMelody(melody1, sizeof(melody1) / 2); break; } //... } //... }
There are only 4 places where soundState is updated
initRobot
Twice in the T_BEEP command handler
T_BEEP_BIN command handler
The test of 'B' command does not invoke the other commands so they should have no effect on soundState.
So it appears that the 'B' command only affects the boot melody. One of these 'B' commands should turn the melody sound off.
send the toggle mute command :('B')
send melody command ('o1') and the melody plays.
send the toggle mute command :('B')
send melody command ('o1') and the melody still plays
What does the 'b' command control?
The 'b' command is the ASCII command to control mute, volume, and play notes. The example from the reference page shows how to play notes:
b 12 8 14 8 16 8 17 8 19 4
The volume of the note is not set by appending a volume value to the command prefix
b2 12 8 14 8 16 8 17 8 19 4
This produces a strange sound.
The single volume setting command is
b2
Issuing this command has no effect I can decern if I then playing a melody
b2 m1
The last form toggles the mute state
b
I note that the code I have says that toggling the mute and unmute prints the "Unmute/Mute" via the printToAllPorts function but that isn't the observed behavior.
The volume command is handled in the reaction function
void reaction() { //... switch (token) { //... case T_BEEP: //beep(tone, duration): tone 0 is pause, duration range is 0~255 if (token == T_INDEXED_SIMULTANEOUS_ASC && cmdLen == 0) //... else { if (token == T_CALIBRATE) { //... } else if (token == T_BEEP) { if (inLen == 0) { // toggle on/off the bootup melody //... } else if (inLen == 1) { // change the buzzer's volume //... } else if (target[1] > 0) { beep(target[0], 1000 / target[1]); } } //... } //... } //... }
This case of interest is the second one, the bVolume form
The 'b' command echos the mute state to the serial port.
void reaction() { //... switch (token) { //... case T_BEEP: //beep(tone, duration): tone 0 is pause, duration range is 0~255 if (token == T_INDEXED_SIMULTANEOUS_ASC && cmdLen == 0) //... else { if (token == T_CALIBRATE) { //... } else if (token == T_BEEP) { if (inLen == 0) { // toggle on/off the bootup melody //... } else if (inLen == 1) { //change the buzzer's volume buzzerVolume = max(0, min(10, target[0])); //in scale of 0~10 if (soundState ^ (buzzerVolume > 0)) //only print if the soundState changes printToAllPorts(buzzerVolume ? "Unmute" : "Muted"); soundState = buzzerVolume; i2c_eeprom_write_byte(EEPROM_BOOTUP_SOUND_STATE, soundState); i2c_eeprom_write_byte(EEPROM_BUZZER_VOLUME, buzzerVolume); PTF("Changing volume to "); PT(buzzerVolume); PTL("/10"); playMelody(volumeTest, sizeof(volumeTest) / 2); } else if (target[1] > 0) { //... } } //...
So the variable buzzerVolume is what controls the volume. The beep function passes the (reduced) variable to the tone function
void beep(float note, float duration = 50, int pause = 0, byte repeat = 1) { if (soundState) { for (byte r = 0; r < repeat; r++) { if (note > 0) { tone(BUZZER , BASE_PITCH * pow(1.05946, note) , duration , buzzerVolume / amplifierFactor ); } else { //... } //... } } }
The tone function passes the value down to the PWM code which use the value to modify the PWM signal, I'm not going into that code now other than to say that it has discernable effect to my ear. This may be due to the speaker type on my robot. I believe it's passive so it doesn't perform the amplification.
I'm not fully satisfied with the analysis above but it has prompted me to get the opencat_serial program running to make the testing easier.