top of page
  • ZackW

Automatically Scored Cornhole - Scorecorn - Part 3

This is part 3 of my capstone saga. You can see part 2 here, and part 1 here. I'm in my final term of capstone now, and have lost one of my teammates due to him graduating (congrats to him at least!). So, now there's just 2 of us. In this part, I'm going to keep going over the ranging code, but also including some of the other components like the loadcells and game algorithms. We're going to be leaving a lot on the cutting room floor, since circumstances have essentially destroyed our ability to complete the entire project.



Picking Back Up on Ranging

The last thing I did was write a lot of code in order to essentially repeat the same functionality as the examples, but with much better scale-ability. Now I need to actually range to multiple anchors, and get the distance to each of them. The next biggest feature is probably the ability to set up the timing between devices automatically. The tag has to find an anchor, tell it when to transmit, and then correct any phase drift that occurs.


In reality, since we're so pressed for time on this project, I'm probably not going to be doing any kind of shutdown and phase drift correction. That is one of the ideal approaches that had to be laid by the wayside in order to get something working. We should have enough battery power that the devices could remain in an active TX or RX state for a long time, maybe a couple hours. So yeah, each device just enters receive mode after transmitting and it can receive a message at any time.


To get the ranging to work, I had some weird conversion issues to sort out. The example code already provided a pretty decent range calculation and was already passing data back to the main control board. The data was garbled along the way though, and I was having trouble getting it to read out correctly. In the end, I ended up with this bit of interesting code to grab the range from the range report packet:

Note that the range is actually reported in mm - the meter distance times a thousand. This makes it easy to pass around with these microcontrollers (and with C/C++!). I still had to cast the number to a double and use a decimal-point '1000.0' in order to have it correctly convert back to meters on this receiving side. In any case, I have a full range report now from any specific anchor:





Jumping Ahead

In the interest of time, I decided to hard-code the "pairing" process and just get the round-robin ranging working with a few devices for the report.

This works by basically tying each task end-to-end, and having each one call the next upon completion. This should work especially well if the anchor never shuts down, and just checks the requested device address. Ideally the anchor would enter sleep mode to save battery life (especially since the receive mode is so current intensive), but in the interest of results we can substitute that feature for a larger battery.

Above image is an example of the polling callbacks used to string the ranging events together.


More Pairing

[Time jump forward]

In the end, I went with a fairly basic pairing system. It's similar to the approach I found and described in part 2. Basically the anchors can all hear the "pair" request at the same time, and will respond at different times. The different times can be random, or they can be hard-coded. I'm hard-coding them, but including a way to turn on the anchors one after the other and have the 'tag' (the main board) repeat the PAIR request.

The PAIR sender code

As shown above, the 'tag' (again, the central point that reads the locations of the game bags) has a task using the TaskScheduler Arduino library that runs on a timer. It sends out a PAIR request, waits a bit for responses, handles any anchors that responded, and then repeats that process until it either hits timeout or found all of its 'anchors' (the game bags!).


With the pairing process working, and the range reports working, we're finally ready to actually start putting together the game code itself!


Tying In the Game Code

I got some help with this part actually. My brother in law, Josh, volunteered to help with the main game loop while I was working on the bag code and my classmate worked on the load-cells. This screenshot shows his comment explaining the approach he took (we discussed it at length before he wrote any code):

It's a fairly simple approach, and we agreed that it would be best to start with something like this in order to just get things working before the end of the term. The game loop will depend on the 2 most recent distances we've received from the bags (the 'anchors'), which are the variables 'new_distances' and 'last_distances', 'old_distances'


I originally thought he made a mistake here in the code he sent me:

The game_loop isn't supposed to run until things are in a steady-state, so if the 'all_is_still' function returns 'false' then the game loop should return and wait to be called again. I thought he had accidentally just copy and pasted the same function at one point and forgot to delete it. Then I noticed one of them was checking if it was in a steady state (last vs new), and one of them was checking if it was in the 'old_distances' state. I'm still not sure exactly what this does, since it only seems to be set and read once...? That bears further study.


To actually implement the game code properly, I sort of need ways to test the game loop with fabricated inputs and see what it comes up with. I started this process by grabbing serial input from the serial monitor on the computer - using the handy Serial.read() and Serial.readBytes() functions. Setting the weight on the board using that process might be good enough, but simulating a bag toss is sort of difficult since it requires sending a series of different distances and then having the delta settle down to the acceptable range to count steady state ('all is still').


My intuition says having a parametrized event you can fire over the serial bus would be a good way to simulate a bag toss. This would be like having "virtual" bags you can control on the computer. Might actually be an interesting game element to explore in the future. But yeah, a bag toss could randomly land on the board and approach with different speeds and such.


It's about around this point that I wish I used more object-oriented programming. Having some of this in classes to be instantiated and deleted might have made more sense.


[sometime later]

The approach I've taken simulates a bag throw by generating an array of somewhat-noisy linear numbers from 8m away to some arbitrary distance to the board. The command (vXXXXiXX[n/y][n/y]) is fairly convoluted but it works well. The first number (XXXX) is the end goal distance, the next is the bag index, and the two y/n options are for whether or not the weight is increased on the board and if the line sensor is triggered respectively.


This seems to work pretty well, the commands make it possible to basically play a "fake" game, but on the final hardware. That should make it pretty easy to manipulate the game while testing the setup. I should probably add a couple more commands to better control the game, and one to reset the game state to a fresh state. So far, I can print the bag states:

But I can't print the weight on the board (I can set it), and I can't view any kind of scoring per team or otherwise. Yet. At this point I should probably throw together as much of the hardware that I can, and try actually getting some results finally.



The 'all_is_still' Function

This function takes in the current set of distances, and the last set of distances, and reports if anything has changed. Obviously there is some noise in the readings, so he used a little range that any changes are ignored and are counted as 'still=true' - nothing has changed. I've gotta say, this is more elegant than how I would have written it lol!

I usually write code very literally, as my brain comes up with the idea I want, so imagine me writing a series of if statements to check if it's less than or greater than the noise 'distance' (divided by 2).


The 'calc_bags_on_board' Function

This function is pretty simple, it takes the given weight (a sum'd reading from the loadcells) and divides it by the expected bag weight to get the number of bags on the board. The fancy for-loop is to take into account an arbitrary number of bags, which is useful for testing. It's cute actually: In each iteration it multiplies the weight of one bag times the loop variable, and the extra bit of arithmetic (BAG_WEIGHT / 2 + 1) appears to be the error margin. So if the bag weighed 10g, the error margin would be 5+1g - 6g. In a perfect world, you'd just check if the multiplied weight was equal to or less than 0 I believe.



Hardware

I should have something like 4/8 bags worth of hardware to build. I might have the parts for a 5th or 6th. If the Nordic semiconductor boards could be made to work, that'd be an easy 2 more bags. The goal is to have 4 bags set up though, and the remaining bags can be "played" by both throwing them and typing their distance in with the virtual throw function programmed earlier.



GUI & Getting Everything Working


Waiting for a Steady Weight

Before implementing this, the game loop starts recalculating the score when the distances have stabilized (the bags have stopped moving). The problem is, the weight of the board actually updates slower than the distances by quite a lot. I believe the standard reading speed of the HX711 boards is just 10 updates per second. The solution should be to implement a similar function to the 'all_is_still', or to include the weights into that function.





18 views0 comments

Recent Posts

See All
bottom of page