**TIM - Timely Inventory Managment** **Yaseen Alkhafaji, Ki Fredeen, Julian Gomez, Sidne Gregory** Overview =============================================================================== Our goal in this project was to develop a smart inventory system that allows for streamlined item distribution. One particular use case that inspired this idea is the current approach to distributing electronic components during 6.08 lab sessions. Often, there is a repository of identical components, but as a student, obtaining these parts can be time-consuming due to the bottleneck of kerberos verification with course staff. Our system aimed to naturally streamline this bottleneck by using MIT ID Cards to verify identity. Our system utilizes an RFID scanner and a touchscreen UI to identify valid students, which communicates with our backend server to identify which users need which items. When a student scans their ID, their ID is checked against a database, and if there are items that the student needs, each relevant bin will light up with a blinking LED. Once the student gets their items, they can tap “done” and the next student can take what they need. Additionally, we utilize the Google Sheets API to keep track of inventory and students that have taken items. Instructors add items and check remaining inventory using the specified Google Sheet. We hope that this project will be useful in the future! System Diagrams =============================================================================== ![](./images/system_diagram.png width="600px" border="0") ![](./images/card_reader_diagram_state.png width="500px" border="0") Documentation =============================================================================== Google Sheets ------------------------------------------------------------------------------- All of the interactions with Google Sheets are governed through the server. On the server, we utilize the Google Sheets API in order to facilitate communication between these two parts of the system. In order to begin utilizing this API, you must first register and configure your credentials: https://developers.google.com/sheets/api/quickstart/python We decided to use the python API for our project, but there are a variety of different languages the API works with. From this page you follow the instructions on how to set up your credentials, and place the appropriate file (credentials.json) that they ask you to download in your working project directory on the server . You should then continue to follow the instructions for setting up quickstart.py . After this step you should be ready to write queries to the API. In any file that you choose to run the API, the following code will always be necessary: ```python with open('/project_directory/token.pickle', 'rb') as token: creds = pickle.load(token) service = build('sheets', 'v4', credentials=creds) sheet = service.spreadsheets() ``` This allows access to the sheet object from which all calls will be made. Before we dive into the API calls, we must understand two values that will commonly appear: **spreadsheet_id** and **range_name**. **spreadsheet_id** is the ID of the Google Sheet that you want to read/write to. It can be found in the URL of your sheet. In the below example from our database, the bolded text is our spreadsheet id: docs.google.com/spreadsheets/d/**1En05jStMRc66i5xwm89072WdZLnCahgnGO1opn1clwM** The other value is **range_name**. This specifies the range of cells that you will read/write to in an API call. They must be written in A1 Notation. The following explanation is pulled straight from the Sheets API documentation: Some API methods require a range in A1 notation. This is a string like Sheet1!A1:B2, that refers to a group of cells in the spreadsheet, and is typically used in formulas. For example, valid ranges are: * Sheet1!A1:B2 refers to the first two cells in the top two rows of Sheet1. * Sheet1!A:A refers to all the cells in the first column of Sheet1. * Sheet1!1:2 refers to the all the cells in the first two rows of Sheet1. * Sheet1!A5:A refers to all the cells of the first column of Sheet 1, from row 5 onward. * A1:B2 refers to the first two cells in the top two rows of the first visible sheet. * Sheet1 refers to all the cells in Sheet1. ![Figure [Sheet in a Sheet]: The sheets tab at the bottom of the page on your spreadsheet, here is an example from our project.](./images/sheets_table_example.png width="400px" border="1") One clarification to be made is the "Sheet1!" that comes before each range. This specifies the specific sheet within the whole spreadsheet to be searching on, as you can have more than one. The primary functionality used in our project, and of the API in general, are reads and writes. **Reads** are very straightforward: ```python result = service.spreadsheets().values().get( spreadsheetId=spreadsheet_id, range=range_name).execute() ``` This returns a 2D list where the outer list is rows of the desired range, and the inner lists are the columns of those rows. ![Figure [Sheets Example]: An example screenshot from our database.](./images/sheets_example.png width="400px" border="1") For example, a read to the figure above of the range "A2:B3" would yield: ```python [["Hershey's Kiss","1"]["Nasty Candy","2"]] ``` Note that all values are strings. **Writes** are slightly more difficult: ```python values = [ [ # Cell values ... ], # Additional rows ... ] body = { 'values': values } result = service.spreadsheets().values().update( spreadsheetId=spreadsheet_id, range=range_name, valueInputOption=value_input_option, body=body).execute() ``` Essentially what is happening here, is that you are putting in data in the form described above (which is also the form returned by the read request) in a specific location. It then overwrites anything in the given A1 Range with the specified values. The range size *must* be correct for this write to work, otherwise the write will fail. So that's basically it for Sheets API usage. There are of course other functionalities provided in the API which I recommend reading: https://developers.google.com/sheets/api/. However this setup in conjunction with these commands formed the bulk of our project. An unforunate reality of the API is that it is very "dumb", it doesn't have many methods that have builtin logic to detect empty cells, or things along those lines. As a result much of the data processing happens on the backend with the server. SQL Database ------------------------------------------------------------------------------- ![Figure [Database]: A diagram of the SQLite database](./images/DatabaseDiag.png width="900px" border="0") To store data that the ESP needs on the server, we use four tables in a SQLite database. Using a database allows us to reduce the number of calls to the Google Sheets API, compared to only using the Google Sheets to store information. The database also allows us to use data more flexibly than with calls to Google Sheets. Our system has three tables that store user information, item information, and item request information (who took and returned what) respectively. The server uses the information stored in these tables to determine whether or not a user is allowed to take items. The user information table stores the raw hexadecimal data from the MBTA's RFID chip in the MIT ID. This data is unique to every card (the MBTA would have problems otherwise), so we can reliably identify registered students using this information. Every user entry in the database has a admin flag that marks a user as an admin. Admins are able to register users at the ESP with their ID. The item table stores the name of an item and which bin it is stored in. The ESP uses the name to inform users what they're taking, and the bin number to choose which bin to light up. The bin is also used in communication with the ESP32, since the name may be changed in Google Sheets. The table also keeps track of item stock and returnability. To differntiate between different item entries, which could have the same names and bins, we use a primary key. Primary keys are unique to each entry in a table, and allow us to use SQLite commands like `REPLACE`. `REPLCACE` modifies an entry or creates a new one if one matching the conditions in `WHERE` aren't met in any rows. Primary keys are declared with `PRIMARY KEY` in a `CREATE TABLE` statement, and are indicated with a "PK" in the above diagram. The requests table stores the student ID and item ID for every item taken. We also store a boolean value for wether or not the item has been returned. This table simply lets us determine if a user has taken or returned an item. Although not used in our implementtation, we included timestamp columns for both taking and returning, for use by the class staff. The fourth table only has one entry, which stores the current lab number. Using a table like this allows us to store one piece of data across calls to the server without additional calls to Google Sheets or using additional packages. The lab number, taken from Google Sheets, is used to limit the items displayed on the ESP to the items from current and previous labs. HTTP requests ------------------------------------------------------------------------------- ![Figure [HTTP]: A flow chart of the HTTP requests made to the server](./images/HTTPFlow.png width="900px" border="0") Due to the limited processing power of the ESP32, it helps to reply to requests with simple messages. We chose to put single letter responses like `i` or `v` for basic decisions at the front, like which state to change to. Other information, like rejection messages or lists of items to parse, would be appended after a colon, like `:Not registered!` or `:Starburst,Gum`. To choose which simple response to use, a query is run, and the decision is usually made based on the number of rows returned. An example query to check if the given user is authorized might look like: ```SQL SELECT studentID FROM approvedUsers WHERE studentID=?; ``` If no rows are returned, then the user is not registered, and the ESP would receive the message `i:Not registered!` from the server. This kind of query is used in the getRequest.py GET response to check if a user is registered or an admin, and in the tapRequest.py and returns.py POST responses to check if a user is registered and allowed to take or return their selected items. When a list of items is required (such as when the ESP needs to know what items to display), a more conventional query, where the list of returned items is actually used, is run. SQLite3's return value isn't very ESP-friendly so we add each item to the return string separated by a comma. This produces strings that are easily parsable with `strtok()`. These queries are used in the tapRequest.py and returns.py to build and return a list of item bins that the given user can take from. Overall, the database is the core component of the server-side part of our project. It enables us to quickly respond to requests to take items and store information across HTTP requests. Additional Parts ------------------------------------------------------------------------------- For this project we used a few additional parts that are not included as part of the 6.08 base kit. These parts included: + A touchscreen display (figure 4) + A RFID scanner (figure 5) + Tray or containers for parts + As many LEDs (figure 6) as containers you will have ![Figure [touchscreen]: TJCTM24028-spi Touchscreen TFT display](./images/touchscreen.png width="200px" border="0") ![Figure [RFID]: RC522 RFID scanner](./images/RFID_scanner.png width="200px" border="0") ![Figure [LEDS]: LEDs](./images/LED.png width="200" border="0") Hardware Setup ------------------------------------------------------------------------------- In the previous section the additional parts were introduced. This section will explain how they were actually implemented into the final system. Let's begin with the wiring. In order to have all of these parts functioning together as a system they must all be wired up to the ESP. Wire up all of your parts like shown in the figures below. ![Figure [Touchscreen wiring figure]: Touchscreen connections to the ESP](./images/Touchscreen_wiring.png width="600px" border="0") ![Figure [RFID wiring figure]: RFID scanner connections to ESP](./images/RFID_wiring.png witdh="500px" border="0") The RFID scanner and the Touchscreen share a lot of the same pins. This is how it is intentional. Just make sure that all of the chip select (CS) pins are wired to different pins on the ESP. ![Figure [LED wiring figure]: LED connections to ESP](./images/LED_wiring.png witdh="500px" border="0") You can wire the LEDs to any IO pin on the ESP that you want to as long as something isn't already wired to that pin. Our system had 6 bins but you could add in more or use less just make sure you set all of the pins up in your code with pinMode(OUTPUT) so you can write them HIGH later on to turn on the LEDs. The LEDs should not be attached directly to the ESP. Make sure to insert a resisor into your wiring like shown in Figure 9. We used and recommend 330 $\Omega$ resitors but you could use a different value if you don't have those. You just want to make sure the resistance is small enough that only about 10-15 mA are flowing through the LED. ![Figure [Picture of system]: Physical wired system (The LEDs are connected to the red and brown wires so they can reach the bins)](./images/System_pic.png width="600px" border="0") After wiring there are a few more things that you need to do to get your hardware up and running. The first is make sure you have the correct libraries for the touchscreen display and RFID scanner. You can download them using the links below. If you have been using our base kit you should already have the TFT_eSPI library for the touchscreen and you just have to make a few modifications. [Touchscreen library](https://github.com/Bodmer/TFT_eSPI) [RFID library](https://github.com/miguelbalboa/rfid) To configure your library for the touchscreen we are using you need to go into the library on your computer and open the User_Setup.h file. Once that file is open make sure that the ILI9341_DRIVER is the one uncommented. Then make sure your TFT_WIDTH is defined as 240 and the TFT_HEIGHT is defined as 320. Next scroll down to where the pins are defined for the ESP32 and uncomment the TOUCH_CS pin and define is as 17. This is the pin we wired our T_CS to earlier. Make sure you declare and initiate both the RFID and TFT in your code otherwise neither of them will work properly because they will not know which one you are trying to communicate with. Energy Management ------------------------------------------------------------------------------- This is not a portable system so presumably it would be plugged in to the wall or a computer for power instead of our battery pack so that it could be continuosly running through a full day of labs and for office hours but it is possible to have it run off of the battery. Through testing we know the system uses about 180 mA on average so using our 1500 mA/h battery it should last about 7.9 hours. This is with the screen at default brightness all of the time and the RFID scanner continuously running. If you wanted to save some power and lengthen the battery life you could dim the brightness of the screen until a tap is detected and the UI display is needed. From the datasheets of our hardware we know that the touchscreen uses about 100 mA the RFID scanner uses about 20 mA and that leaves about 60 mA for the ESP on average. Code Description =============================================================================== heartbeat.py ------------------------------------------------------------------------------- heartbeat.py serves as the mechanism by which all updates occur to the sheets in real-time. The ESP sends a GET request to this endpoint peroidically, which causes its script to run: updating all of the stock values and items individuals have taken out. It is comprised of two core functions aptly named: stock_update() and student_sheets_update(). The stock_update runs code that pulls stock data from the database which is being modified in real time, then sends a write request to the sheets in order to display the newly changed data. Student sheet update does something similar, except it finds any students in the database who are registered users, then checks if they have any items out. After this is performs the same update, writing to the sheet the information. items.py ------------------------------------------------------------------------------- items.py serves as the startup script to the entire system in which all "item logic" is determined. The ESP makes a GET request to this endpoint on the server during its setup process, and the script returns to the ESP the bins and item names for the current lab as determined by a cell in the sheet. It is returned in a preformatted form as to make string parsing easier in the ESP e.g. "1,2,3|Item1,Item2,Item3". It also updates the database with all values from the sheet on startup. It takes the item_names, bins, stock, , returnable flag, and current lab and writes that data to the items table in the database. userRequest.py ------------------------------------------------------------------------------- userRequest.py serves as the interface for checking and registering users. A GET request, given a user ID, will: check to see if the user is registered, returning a `v`; check if they're an admin, returning an `a`; and returning `i:not authorized!` otherwise. A POST request will register a user in the database, given a user ID. A POST request will return `v` for successful registration, and `i:Already registered!` if the userID is already registered. tapRequest.py ------------------------------------------------------------------------------- tapRequest.py is the interface used to take items. A GET request, given a user ID, returns a comma separated list of bins the user is allowed to take from, based on what they have taken already, or `i:not authorized!` if they are not registered. A POST request, given a user ID and list of bins, will record an entry for each item taken in the `requests` table, and return `v` if successful, `i:not authorized!`, `i:already taken one of these items!`, or `i:selected nothing!`. returns.py ------------------------------------------------------------------------------- returns.py is structured almost exactly like tapRequest.py, except for returns. All the same checks are made in this script. To register a return, the `requests` table entry is modified so that the returned flag is true. ESP32 Code Overview ------------------------------------------------------------------------------- ![](./images/ESP_Code_SM.png width="600px" border="0") Pictured above is the state machine diagram for all of the code running on the ESP32. This diagram covers most of the control flow of the code, so below we will include an overview of each state and method in the ESP32 code. ### State Machine descriptions It is important to note that loop() is called as often as the ESP desires, but updateScreen() is invoked in loop() only if the global variable ```shouldUpdate``` is true. #### READ This is the "main" or "idle" state. In **loop()**, we wait for a **valid** RFID tap. An invalid tap simply results in the method returning early. If a valid tap is detected, then the UID string is read from the RFID sensor library and stored in a global ```char array``` called ```idnm```, and the state is set to TAKE_OR_GIVE. ```idnm``` is currently a constant size of 20, but this can be played with. All of the MIT ID cards we tested were found to be 8 characters long, but it's possible for the RFID string to be longer. When we tried it on a random Metro card, it was 16 digits. As such, we increased the buffer size to accommodate for more flexible RFID options. In **updateScreen()**, we clear the screen and display the string ```"Tap card to start!"``` onto the TFT. #### TAKE_OR_GIVE This is the state in which we let the user decide if they want to take items, return items, or go to the admin page. In **loop()**, we look for touchscreen taps using **tft.getTouch()**, which returns true if a tap was detected, and which updates local integers x and y. We then check to see if the x and y value of the tap fall within any of the 3 options, or if "Done" was tapped. If done was tapped, we set the state to READ and set shouldUpdate to true. If "Return Items" was tapped, we set the global boolean ```taking_items``` to false. If any of the three transaction boxes were tapped, we transition to the CHECK_NEEDED state and set shouldUpdate to true. In **updateScreen()**, we draw 3 rectangles with the same left x value, width, and height; each corresponds to an option for inventory manipulation. Additionally, we draw a "Done" button in case the user would like to cancel their transaction. #### CHECK_NEEDED This is the state that authenticates a user and decides which items they need to take (or return). In **loop()**, we first invoke ```send_userGET_request()``` which sends the RFID tag to the server. This HTTP request is blocking. Once the request is done, we check the first character of the response buffer. If it starts with "i", then the request was invalid, so the state is set to REJECT and the global variable led_start_time (used for displaying the rejection message) is initialized to millis() epoch time. If it starts with "a", then the user is an admin, so we set the state to ADMIN. If it starts with "v", then the request is valid, and was either a return request or a take request. We call ```updateNeededBins()```, which parses the response buffer to see which bins the user needs to take/return items to/from. Then, the state is updated to either "TAKING_ITEM_SELECTION" or "RETURNING_ITEM_SELECTION", depending on whether the user was taking items or returning items. The UI page display variable is also set to page 0, and ```last_tap_time``` is set to millis() epoch time. If it doesn't start with any of these letters, then something went wrong (either the server did not return an expected response buffer, or the response buffer data somehow got corrupted), so we set the set the state back to READ. In all cases, we set shouldUpdate to true (this state always transitions to another state). In **updateScreen()**, we clear the screen and display the string ```"Checking DB for user..."``` onto the TFT. #### TAKING_ITEM_SELECTION In this state, we are waiting for the user to tap the items that they want; each tap keeps the system in the same state, but sets shouldUpdate to true, so that the TFT can show that the item has been selected. In **loop()**, we first check if millis() - last_tap_time is at least TAP_DELAY. This is to ensure that a tap gesture is not interpreted as multiple taps (because a user can deselect an item). We then scroll along the box coordinates and check if a user has tapped in any box's area. If they have, then we increment the bin corresponding entry in ```ledTrigs``` by 1. If either of the page arrows were tapped, then we update the page variable accordingly. If the done button was tapped, then we iterate through the ```ledTrigs``` array to see which bins' entries are odd-numbered. Each odd-numbered bin corresponds to a selected bin. If at least one bin was selected, we set state to SENDING_REQ, else we set it to READ. In **updateScreen()**, we check if the user needs any items. If they don't, then we clear the screen and display a Done rectangle as well as text telling the user to tap done. Else, we draw 4 rectangles for each item on the current page. Depending on the value in ```ledTrigs```, the box may be red (indicating unselected) or blue (indicating selected). We also render arrows on the bottom depending on the current page and the number of needed items. #### RETURNING_ITEM_SELECTION This currently is identical to TAKING_ITEM_SELECTION. However, we distinguished the states in case there was any desire to expand the project in the future. #### SENDING_REQ This is the state that prepares, sends, and handles the POST request for taking (or returning) items. In **loop()**, we call ```condenseSelection``` which prepares the items section of the POST request body. We then invoke ```send_POST_request()```, which compiles the whole POST request body and sends it. This HTTP request is blocking. Once the method is done, we check if the response buffer starts with a "v". If it does, we set the state to VALID and light up each of the selected bins by indexing into ledArr using ```bins_selected```. We also initialize ```led_start_time``` to be millis() epoch time. If it does not, then we set the state to REJECT and set led_start_time to millis() epoch time. In all cases, shouldUpdate is set to true. In **updateScreen()**, we clear the screen and display the string ```"Sending req..."``` onto the TFT. #### VALID This state represents a VALID request to take items. In this state, the LEDs are lit up, and occasionally, the heartbeat script is pinged. In **loop()**, we first see if millis() - heartbeatTimer is at least HEARTBEAT_PERIOD. If it is, then we set heartbeatTimer to millis() and invoke send_heartbeatGET_request(), which pings the heartbeat script with an HTTP GET request. We then wait for either millis() - led_start_time to be at least LED_HOLD_TIME, or we wait for any tap to the touchscreen. Either condition triggers the transition back to the READ state, which turns off the LEDs. In **updateScreen()**, we clear the screen and display either ```"Please take your items. Enjoy!"``` or ```"Thank you for returning your item(s). Sharing is caring!"``` onto the TFT, depending on if the user was taking items or returning items. #### REJECT This is the state that displays rejection messages, after which it transitions back to the READ state. In **loop()**, we wait for either millis() - led_start_time to be at least REJECT_MSG_TIME, or we wait for any tap to the touchscreen. Either condition triggers the transition back to the READ state. In **updateScreen()**, we clear the screen, invoke ```getRejectionMsg()`` (which parses the rejection message from the response buffer), and then display the rejection message string onto the TFT. #### ADMIN This is the state in which an authorized admin can register new students. The ESP waits for the admin to either tap "Register new student" or "Done", which trigger different state transitions. In **loop()**, we invoke ```getTouch()``` and see if the tapped coordinate is in either box. If it is, then we set the state to either NEW_USER (if "Register new student" was tapped), or READ (if "Done" was tapped). If either button is tapped, shouldUpdate is set to true. In **updateScreen()**, we clear the screen and draw rectangles for the two buttons. #### NEW_USER This is the state that registers a new user once they tap their card. In **loop()**, we wait for a valid RFID tap (in the same manner as the READ state), and once a valid RFID is read, we update ```nidnm``` with the UID and call ```send_registerPOST_request()```. This (blocking code) sends a POST request that registers users. regDisplayTimer is then set to millis() epoch time, the state is set to REG_RES_DISPLAY, and shouldUpdate is set to true. In **updateScreen()**, we clear the screen and display the string ```"Tap Card to Register!"``` onto the TFT. #### REG_RES_DISPLAY This is the state that displays the result of a student registration. In **loop()**, we check if millis()-regDisplayTimer is at least REG_DISPLAY_TIME. If so, then set the state back to ADMIN and set shouldUpdate to true. In **updateScreen()**, we clear the screen and check the first character of the response buffer. If it starts with a "v", we display the string ```"New Student Registered"``` onto the TFT. Otherwise, we invoke ```getRejectionMsg()``` and display the rejection message. ### C++ Method descriptions #### setup() Native ESP/Arduino method. This initializes Serial communication, the TFT touchscreen, and the WiFi module. Additionally, it calls ```send_initGET_request()```, which informs the ESP of all existing items. It then sets the pinMode of all LEDs to OUTPUT, and sets the state to READ. #### loop() Native ESP/Arduino method. This runs the state machine and invokes ```updateScreen()``` if shouldUpdate is true. #### send_POST_request() Arguments: char pointer itemz, char pointer id, bool taking Blocking method. This sends an HTTP POST request to either tapRequest or returns, depending on ***taking***. It appends itemz (a string of comma-separated bin numbers) and id (RFID UID) to the POST body. This then calls the ```do_http_request``` method written in lab, and updates the response buffer accordingly. #### send_registerPOST_request() Arguments: char pointer id Blocking method. This sends an HTTP POST request to userRequest. It appends id (RFID UID) to the POST body. This then calls the ```do_http_request``` method written in lab, and updates the response buffer accordingly. #### send_heartbeatGET_request() Blocking method. This sends an HTTP GET request to heartbeat. This then calls the ```do_http_request``` method written in lab, and updates the response buffer accordingly. #### send_userGET_request() Arguments: char pointer id, bool taking Blocking method. This sends an HTTP GET request to either tapRequest or returns, depending on ***taking***. It appends id (RFID UID) to the GET arg URL. This then calls the ```do_http_request``` method written in lab, and updates the response buffer accordingly. #### send_initGET_request() Blocking method. This sends an HTTP GET request to items. This then calls the ```do_http_request``` method written in lab, and updates the response buffer accordingly. Once the response is received, we invoke ```updateBinsAndItems()```. This method does our "hard reset" GET request, and therefore only needs to be done once during setup(). #### updateNeededBins() This method re-initializes the global int ```num_needed_items```, which represents either the number of bins to return, or the number of bins to take, depending on what the user specified in the TAKE_OR_GIVE state. This method parses needed items from CHECK_NEEDED in the same manner as updateBinsAndItems(). The only difference is that the response buffer starts with "v", and is therefore in the form "v:x,y,z|Item 1, Item 2, Item 3". The "v" means that the request was valid; the rest of the string corresponds to bin<-> item mapping in the same manner as updateBinsAndItems(). As we parse the expression, for each next item, we increment num_needed_items. Additionally, we update the ```needed_item_bins``` array at index num_needed_items to be the current bin number MINUS ONE. (This distinction is important: the bins are 1-indexed on Google Sheets/the database, but they are 0-indexed on the ESP side, so that bin numbers directly translate to array indices). #### updateBinsAndItems() This method initializes the global int ```used_bin_count```, which represents the total number of bins used from lab 1 to the current lab (this is general, and includes all bins that have been used so far, such that if a new user is added in the last week of class, they should be able to get all of the items since the first day of class). The response_buffer from send_initGET_request is in the form "x,y,z|Item 1, Item 2, Item 3". We use strtok to parse this expression. The string maps bins to item names (specifically, this says that "bin x has Item 1 in it, bin y has Item 2 in it, and bin z has Item 3 in it"). When we were designing the system, we initially had trouble deciding how to map items to bins. As our ultimate design choice, we decided that each bin would be unique (could not be re-used), and that item names did not need to be unique. As such, this method reflects this methodology. As we parse the expression, for each next item, we increment used_bin_count. Additionally, we update the ```used_bins``` array at index used_bin_count to be the current bin number (this uses 1-indexing). The used_bins array is not currently in direct use by any code, but it stores the bin<-> item mapping information on the ESP, so it could easily be used in the future, in case an admin wanted to debug something or wanted the ESP to have full knowledge of all items. As we update used_bins, we also the ```items``` 2D char array so that the char array at each (0-indexed) bin has the name of the item for that (1-indexed) bin. #### condenseSelection() Arguments: int pointer selectedItems, int sz, bool names This method condenses each of the **sz** selected items in the **selectedItems** array to a string that can be used in the POST body. Depending on if **names** is true or false, it will either use bin numbers, or item names. (the flag depends on the design choice: for a while, we assumed item names were unique and bins were not unique, and as such, we sent item names in the post request; in later revisions, we swapped the roles so that bins were unique while names weren't, and as such, we now send bin numbers instead). This first clears the global char array items_condensed, and then it iterates over each of the **sz** items in **selectedItems**, and it appends the required string as well as a comma if there are more items to follow. #### getRejectionMsg() Arguments: char* arr This method splits the response buffer, assumed to be in the form "i:Invalid Message Here", into just the string "Invalid Message Here", using strtok. It then uses strcpy to put it into **arr**. #### updateScreen() Arguments: int state All of the display code is written here. This updates the TFT screen using the TFT library. The current display is determined by the ***state*** argument. Design Challenges and Rationale =============================================================================== Heartbeat ------------------------------------------------------------------------------- Our ESP employs a heartbeat script which sends a GET request to a specified endpoint after a designated time period. This endpoint will trigger a script on our database that will update the Sheets data in realtime for staff viewing. It updates the stock numbers, as well as the student : item mappings indicating which students have taken out items. We chose this information, as we felt it was the most pertinent information to provide to the staff in real time: the amount of an item left, and which students have something taken out. We decided to omit other information that we might be able to send, as it increases complexity of the system. The HeartBeat python script is located under **servercode/heartbeat.py**. In the ESP code, it's triggered by the void method **send_heartbeatGET_request()**. Hard Resets ------------------------------------------------------------------------------- This design choice relates to how the database, sheets, and ESP update with regards to a complete restart of the system via ESP restart. We decided that during a hard reset, the system would update all of its values from the Sheets. On the database: the current lab, stock, item names, bin numbers, and returnable tags would be repopulated, and on the ESP: the items and their bins would be repopulated. We decided to have all of these feature changes not be available during real time *from* the Sheet. As a result, the sheets only serve as a visual while the ESP is running, as all of its data will be overwritten by its heartbeat script. It only becomes a mutator of the database + ESP when the ESP resets, where Sheets edits will be used to pull new data. We decided to go with this less robust approach to avoid concurrency issues that might arise from having two systems modifying data at the same time. While the SQL DB has proper locking to avoid this, the Sheets does not, and if a user-side edit were to occur before the script edit, it would very likely display inaccurate information if we allowed concurrent edits. The Hard Reset python script is located under **servercode/items.py**. In the ESP code, it's triggered by the void method **send_initGET_request()**. Design of User Interface ------------------------------------------------------------------------------- Because of the size of our touchscreen display and the fact that the touch sensing is not always right where you tap we decided that the best user interface would be something with big buttons that went all the way across the screen so they were easy for the uesr to select. The size that we decided to make our buttons meant that there could only be 4 items for the user to select on the screen at one time but we expect for there to be more than four items that the user would have the option to take so we had to make a way for the user to access those other items without having to select and take the ones that came before it and then retapping their card and seeing if new items came up. We decided that scrolling would be too complicated both for the user to do without accidentally selecting an item and for us to code so we choose a page scheme where if there are more than four items the user has the option to go the next page and back to the previous one as well.