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.
OK, it have a answer on the boot melody. It occured to me that I never tried the binary command ('B') to mute and unmute. I tried it and it did turn the boot melody back on, Let me review the code to see what the difference is.
Yes, exactly right. The github response refers to the OpenCat codebase for the Bittle /Nybble robot but the Bittle X robot uses the OpenCatEsp32 codebase.
But I can't reproduce the problem:
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
Using the serial command "?" To check the version date is:
This was one of the first things I wanted to do as well. The result was that on powerup the melody no longer plays but I still get a loud chirp confirming each command (IR or serial). I then tried the melody command to check if the volume was changed but it's just as loud (when I use b0). I haven't been able to restore the default volume setting so that the melody plays on powerup..
Is this a per session setting or is it saved in permanent memory? Does it affect all sounds or just notes?
As I understand it, the github response refers to the OpenCat codebase for the Bittle robot but the Bittle X robot uses the OpenCat32 codebase. My Bittle X console output seems to match the output for OpenCat32 (e.g., the string "Calibrated Zero Position" only appears the OpenCat32 codebase). So my analysis here is for the OpenCat32 codebase. If I'm offbase here, please let me know. However I note that not all the output from OpenCat32 appears in the cosole so there's some discrepancy..
First I do a volume test:
send the play melodycomnand ('o1') for the baseline volume
set the volume to 3 ('b3')
send melody command again with no difference in volume
Then I do a toggle mute
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
The codebase I have says that setup calls initiRobot
void setup() {
//...
initRobot();
}
The function initRobot reads the value from EEPROM memory and then calls i2cEepromSetup
I have more analysis but the test results indicate the birthmark has been changed. The problem is that code also says that the birthmark is rewritten to the default value BIRTHMARK by the function initRobot 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
//...
}
This update happens unconditionally.
I have more analysis around the soundState value and the birthmark value but I would rather wait until I know if I have to take the plunge and update the firmware.
The only other marker I have is the value of DATE in OpenCat32.h:
Also note the image for the serial protocol specifies that the ASCII beep command requires a note and duration while the binary command does not. The code contradicts this.
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.
OK, it have a answer on the boot melody. It occured to me that I never tried the binary command ('B') to mute and unmute. I tried it and it did turn the boot melody back on, Let me review the code to see what the difference is.
Yes, exactly right. The github response refers to the OpenCat codebase for the Bittle /Nybble robot but the Bittle X robot uses the OpenCatEsp32 codebase.
But I can't reproduce the problem:
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
Using the serial command "?" To check the version date is:
Bittle
B01_240207
Please upgrade to the newest firmware and use the following command:
https://bittle-x.petoi.com/2-open-the-box#adjust-the-buzzer-volume