Pretty Printing

Spring 2020

The questions below are due on Monday February 17, 2020; 11:59:00 PM.
 
You are not logged in.

If you are a current student, please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.csail.mit.edu) to authenticate, and then you will be redirected back to this page.
Back to Exercise 02

Your LCD will Feel Pretty

Normally in 6.08, in this exercise we write some support code that allows our ability to print to our display to properly wrap around the screen. So you instead of you getting something like this (like we had in Lab01B):

Our display cutting off characters

...you get something like this:

Our display wrapping the characters

Interestingly and somewhat surprisingly, it turns out that the LCD and the supporting library we chose to use with it this year (we've used different displays in the past), has this feature built in! To take advantage of it, instead of writing to the screen using this syntax:

tft.drawString("some string",0,0,1);

You can do the following:

tft.setCursor(0,0,1); //set cursor at top left of screen, and set font size to 1
tft.println("message"); //print a whole line.
tft.print("Some text"); //print some text 
tft.println(" more..."); //continue printing from cursor
tft.println("I think that I shall never see, a poem lovely as a tree."); //long line that wraps!

And if you were to run this code you'd get:

The result...

Writing this from scratch was always a bit of a thing, and not necessarily for the better, so we'll skip it this year and instead focus more on char arrays.

1) More on Char Arrays

As you worked through the exercise on char arrays last week, you may have encountered some annoying errors on compiling such as

invalid conversion from 'char' to 'const char*' [-fpermissive]

This usually came about by trying to use strcat or strcpy with a single char rather than with a pointer to a char, which is what the actual variable we use to represent a char array is.

An array of anything in C/C++ is referred to by a pointer to its first element. For example, to work with an int array called my_array, the variable my_array is actually a int* pointing to the first element of my_array (my_array[0]). If you were to use the derference operator on * on the variable my_array (so *my_array), it would be the exact same as doing my_array[0].

The same thing applies for arrays of chars.

For example if we have this:

char message[] = "Hi there!"

What this has done is create

Placeholder for Diagram 467ae36ed1b5256d24aad434b59f26b4
Memory with an integer in it.

In fact, we can actually see evidence of this with the following code you could run on your ESP32. Take a look at the code, read the printout, then read the explanation below it.

char message[] = "Hi there!";

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.println(message);
  for(int i = 0; i<sizeof(message); i++){
    char report[100] = {0}; //make null-filled report array
    int address = (int)&message[i]; //convert the address of char at message[i] to an int
    //what this does is allow us to store the memory address in a readable form

    int value = *(uint8_t*)address; //read address as a uint8_t pointer (meaning...read one byte),
    dereference it to get value
    /*in more detail we take the value of address and then create a uint8_t pointer to it.  This
    * is important because when we dereference it using the * operator in the next step, it will
    * only read one byte. The dereferncing part (the left-most *) reads the value in the memory 
    * address poined to.
    */
    char letter = (char)value;  //convert whatever number is in value into a char
    sprintf(report, "At Location %d is the value %d which is ascii %c",address,value,letter);
    Serial.println(report);
  }

  while(1); yield();
}

If you run this on your device (and you'll possibly get diferent locations when you run yours), you'll get the following printout:

Hi there!
At Location 1073470388 is the value 72 which is ascii H
At Location 1073470389 is the value 105 which is ascii i
At Location 1073470390 is the value 32 which is ascii  
At Location 1073470391 is the value 116 which is ascii t
At Location 1073470392 is the value 104 which is ascii h
At Location 1073470393 is the value 101 which is ascii e
At Location 1073470394 is the value 114 which is ascii r
At Location 1073470395 is the value 101 which is ascii e
At Location 1073470396 is the value 33 which is ascii !
At Location 1073470397 is the value 0 which is ascii 

Things like the Null character can't be printed...but you can see it is null because the value of it is 0!

When a you specify a variable type in parentheses in front of another variable this is the traditional way of casting the value into the other form. This tells C/C++ to interpret the data found at those spots in memory as the type you're casting into. Remember everything is stored as just arrays of 1's and 0's in a computer, and it is our interpretation of those 1's and 0's that gives them meaning. The sequenc 101010 will mean something different if you're interpretting it as an int or an char. So for example in the line int address = (int)&message[i]; we converting into an integer ((int)) the value of the address (&) holding the char of interest (message[i]).

In the line int value = *(uint8_t*)address; we are taking that address variable, and converting it into a uint8_t pointer ((uint8_t*), which is important for what we do next. We analyze the value pointed to, and since we are looking at it through the lens of a one-byte uint8_t, it means we look one byte chunks, rather than in four-byte chunks like you would with an int pointer, int*). As a result when we dereference and look at the value in memory (*) we get the value stored in only that one byte.

Finally in the line (char)value; we are taking the value we previously extracted and going full circle, converting it back into a char by casting it ((char)).

From the printout we can see three things:

  • First is each memory cell is immediately after the other going from memory address 1073470388 to memory address 1073470397.
  • Second is that each memory cell holds a value which ranges from 0 to 127 (this is because these originate from specifying ASCII letters which can only range between that)
  • Third when we convert that value back into a char (by casting), it becomes the letter again when printed.

The letters align properly with what a standard ASCII table prescribes!

The ASCII table!

We will continue to explore arrays and C and how things are stored at the low level in future weeks.

2) A Thing to Do for Today

One thing we'll be doing a lot in 6.08 is storing and retrieving data from servers. When that data comes down to us, it will usually be in the form of a string/char array. However, that data will often have numbers within it that we need to work with. There is a need, therefore, to be able to get our code to stop interpretting the incoming data as an array of character symbols, and instead interpret them into numbers.

If you have a single digit of a char that you wish to convert to an int you actually can't do the following:

char c = '5';
int x = (int)c; //convert c to an integer
Serial.println(x); //prints out 53

This is because the value stored in memory for the char '5' in binary is 00110101 or 53 in decimal. When we cast the char to an int, it literally takes the value stored in memory at that spot and reinterprets it as an integer/decimal. That's a bummer.

Thankfully there is a function that already exists to help with this called atoi (doc right here). If you give atoi a char array full of valid numbers, it can interpret it into an integer for you. For example consider the following code snippet (which you can run in your Arduino ESP32 environment):

int x = atoi("15"); 
Serial.println(x+1); //prints 16
x = atoi("-15");
Serial.println(x+1); //prints -14
x = atoi("+1505");
Serial.println(x+1); //prints 1506
x = atoi("abc");
Serial.println(x); //prints 0
Serial.println(x+1); //prints 1

This mostly works as we'd expect. The only thing to be careful for is if non-numbers are fed it returns a 0, so if you are using that number make sure you know where that 0 is coming from (is it actually 0 or not?). Second, atoi expects a char array and not a single char so be careful on that front as well.

In reading in a string containing numberical information, we'll often also split the numbers using a known symbol. For example, we might receive a char array of the form:

"134&245&111333&89"

We'd want to split the char array at the & locations and use the chars in between to generate out number. The function strtok which is part of the string.h library can help us with that. The documentation for strtok is here, but a quick summary is that this function can split up a char array on specified tokens. You should take a few minutes to read up on this function and how it works. It is a stateful function, meaning it is designed to be called multiple times.

imu placement on board

Diagram showing an example operation of `strtok`.

We made a very nice diagram demonstrating how strtok works for you to consume!

Create a function called int_extractor that has three arguments:

  • char* data_array: An input char array that contains a string of integers separated by a delimiter
  • int* output_values: An integer array to be used as an output to store integers you extract from the intput char array.
  • char delimiter: The character you are to use as the delimiter in chopping up the input string.

You can assume that every char array handed in is properly formatted (no random chars in where numbers should be). You can also assume that output_values will always be large enough to hold whatever is provided by data_array and that delimiter will be found in data_array (if there is data to be extracted). Input data strings will always start with a valid number.

Watch for edge cases like an empty input string.

Consider using code like this as a test platform:

#include<string.h>

int values[10];
void setup() {
  Serial.begin(115200);
}

void loop() {
  char message1[] = "56&14&18&9";
  int_extractor(message1,values,'&');
  for (int i = 0; i<sizeof(values)/sizeof(int); i++){
    Serial.println(values[i]);
  }
  delay(1000);
}
void int_extractor(char* data_array, int* output_values, char delimiter){
    //your code here
}

and test it with different things as needed.

void int_extractor(char* data_array, int* output_values, char delimiter){ //your code here }

Back to Exercise 02