A little while back, 3D Dave was having a bit of a clearout. He gave us a P8Z77-LX motherboard, CPU and Zotac GeoForce GTX graphics card.
A quick hit on eBay and for about fifty quid we had some memory (8Gb) and a (400Gb) hard-drive ordered. An old modular power supply completed the rig and a copy of Windows 10 for just twenty quid at computerbuilders.co.uk meant we were ready to go, and fully legit (really, at these kind of prices, does anyone still mess about with torrents for software any more?)
Windows 10 installs much more quickly (from a usb key) than previous versions ever did; less than an hour later and we had a spanky brand new PC running both Unity and Blender, complete with a twin-screen setup, ready for some serious modelling/rigging/animation.
I've never really understood why Steve raves so much about his multi-screen set-up (three large monitors and a smaller one for emails). But after playing about with two monitors (and a half-decent PC) I can see exactly what he was talking about.
In fact, the only question I have left is - if my graphics cards has twin DIV outputs (currently driving the twin monitor set-up) can I shove a third into the HDMI slot?
Multi-screen computer FTW!
Sunday, 13 November 2016
Wednesday, 9 November 2016
Blood Bowl is back - and maybe, so too, might our electronic board game?
About a million years ago, we made a simple electronic device to keep track of plays and character stats for the tabletop fantasy football game Blood Bowl. At the time, the game was pretty much out of favour, but has since seen not just one, but two digital versions of the game.
Our version wasn't much to write home about, but did start a many-years-long search for a still-as-yet-elusive tabletop/computer cross-over.
We came close. We even made a working prototype and got a massive 48 x 64 square playing surface working. But then creating games for heavily-copyright-protected content was always a bit of an obstacle.
So while we're trying to find a way of releasing the hardware designs under an open source licence, we've been distracting ourselves with creating assets for a potential Unity game. Even if it comes to nothing, 3d modelling is something that we really should have looked at a long time ago but just never go around to it.
With the number of 3d printers and potential for all kinds of awesome tabletop terrain having exploded in recent years, it was really only a matter of time before one of us succumbed and had a go at modelling some game assets. Steve, of course, has been an avid 3DS Max user for a long time - creating all kinds of awesome structures and models for his 3D printer. I decided to go the other route and try to tackle Blender....
Using a number of online sources to get started, I managed to import a couple of pre-bought Unity assets (via the .fbx importer). But what to model....?
Which brings us back to Blood Bowl.
Games Workshop are releasing a new version late in 2016 (there's talk that it might be as early as 19th Nov). It's the original game, repackaged, but with some really nice looking miniatures. Which means it's probably going to get a bit boring after a couple of plays. But it also means that there's a massive amount of interest around it already.
So - if we're going to produce a relatively simple, over-in-an-hour kind of game to put to use with our electronic hardware - why not some Blood Bowl characters? We've already bought some low-poly cartoon Orcs that will look great as fantasy football players.
Unfortunately, fantasy-style humans, in armour, with spikes and sporting American Football helmets are not so easy to come by. So that's where we started.
With a basic human character, I then added some shoulder pads, a helmet, and plenty of spikes. Now Blender is a massive topic and there are no end of online tutorials (plus, I'm still not entirely sure about what I'm doing, so wouldn't like to put together a how-to just yet). But here's a preview of my first low-poly Blood Bowl human, complete with basic UVs applied.
It's a total of 1500 polys, including the optional helmet, shoulder pads, armour and spikes. The idea is that, in game, (if it ever gets made) the player can add or remove armour to reflect the character's playing position. A thrower or a catcher might have little or no armour, whereas a blitzer or a blocker would have plenty of armour (and lots of spikes too!)
In a first attempt at UV unwrapping, we came up with this monstrosity:
But it applied nicely to the 3d model (as shown in the image higher up). What worked really well was simply taking a Google photo and dropping it onto the UV image. In this case, I simply Googled "boot treads" in images, found a photo of the sole of a boot and dropped it onto the UV image.
It's a long way to go before we're producing models up to the standard of those in the Asset store. But for now, we're going to say that the modelling is done, and have a look at rigging and animating our little character in Blender, before exporting it for use in Unity.
Our version wasn't much to write home about, but did start a many-years-long search for a still-as-yet-elusive tabletop/computer cross-over.
We came close. We even made a working prototype and got a massive 48 x 64 square playing surface working. But then creating games for heavily-copyright-protected content was always a bit of an obstacle.
So while we're trying to find a way of releasing the hardware designs under an open source licence, we've been distracting ourselves with creating assets for a potential Unity game. Even if it comes to nothing, 3d modelling is something that we really should have looked at a long time ago but just never go around to it.
With the number of 3d printers and potential for all kinds of awesome tabletop terrain having exploded in recent years, it was really only a matter of time before one of us succumbed and had a go at modelling some game assets. Steve, of course, has been an avid 3DS Max user for a long time - creating all kinds of awesome structures and models for his 3D printer. I decided to go the other route and try to tackle Blender....
Using a number of online sources to get started, I managed to import a couple of pre-bought Unity assets (via the .fbx importer). But what to model....?
Which brings us back to Blood Bowl.
Games Workshop are releasing a new version late in 2016 (there's talk that it might be as early as 19th Nov). It's the original game, repackaged, but with some really nice looking miniatures. Which means it's probably going to get a bit boring after a couple of plays. But it also means that there's a massive amount of interest around it already.
So - if we're going to produce a relatively simple, over-in-an-hour kind of game to put to use with our electronic hardware - why not some Blood Bowl characters? We've already bought some low-poly cartoon Orcs that will look great as fantasy football players.
Although crude, these low-poly Orcs will be perfect for use in a fantasy football game where many characters can appear on screen at the same time - so they'll be pretty zoomed out
Unfortunately, fantasy-style humans, in armour, with spikes and sporting American Football helmets are not so easy to come by. So that's where we started.
With a basic human character, I then added some shoulder pads, a helmet, and plenty of spikes. Now Blender is a massive topic and there are no end of online tutorials (plus, I'm still not entirely sure about what I'm doing, so wouldn't like to put together a how-to just yet). But here's a preview of my first low-poly Blood Bowl human, complete with basic UVs applied.
It's a total of 1500 polys, including the optional helmet, shoulder pads, armour and spikes. The idea is that, in game, (if it ever gets made) the player can add or remove armour to reflect the character's playing position. A thrower or a catcher might have little or no armour, whereas a blitzer or a blocker would have plenty of armour (and lots of spikes too!)
in Blood Bowl, the catchers is much more lightly armoured than players such as Blockers and Blitzers.
In a first attempt at UV unwrapping, we came up with this monstrosity:
But it applied nicely to the 3d model (as shown in the image higher up). What worked really well was simply taking a Google photo and dropping it onto the UV image. In this case, I simply Googled "boot treads" in images, found a photo of the sole of a boot and dropped it onto the UV image.
It's a long way to go before we're producing models up to the standard of those in the Asset store. But for now, we're going to say that the modelling is done, and have a look at rigging and animating our little character in Blender, before exporting it for use in Unity.
Saturday, 5 November 2016
More messing about with IR
We've been using IR quite a bit lately, for a project that involves an object which moves, and tells a "base" object that it's resting in about it's current state. The idea is that the base object lights up to tell the user what state the first object is in.
Because the object can spin, rotate and generally move around, we had to implement a wireless method of sending information between the base and the object. Radio seemed a bit overkill (and, since the object is battery powered, we wanted to reduce the current draw as much as possible). IR seemed like the perfect solution.
In fact, we had a working solution a few weeks back, using tiny reflective sensors. But instead of sending a signal and reading the reflected response, both the object and the base had a QRE1113 sensor inside them - so the base didn't read it's own reflected signal, it received a signal sent from the object.
Everything seemed fine. Except the distance between the sensors was critical. At this distance, everything worked great.
But at this distance...
... even after ensuring that the sensors are perfectly in line, the signal often didn't make it from one to the other. After inspecting the IR LED in the sensor (using a phone camera) the amount of light emitted appears as a tiny, tiny dot. Basically, these reflective sensors are good up to about 8-10mm. Anything more than that and the signal gets lost - or occasionally corrupted.
We wanted to increase the distance between our sensors so needed something with a bit more oomph. A simple IR LED and IR phototransistor seemed like a good idea.
Except these were very sensitive to the angle of two being absolutely accurate. Even just a couple of degrees off and the phototransistor stopped responding to the IR LED.
So Steve suggested using the Vishay 38khz IR receivers, most commonly found in TV units (to decode remote control button presses). These are able to receive IR bounced around from all over the place!
Not only do they work if you point an LED directly at a sensor....
They also work at greater distances, and even at 90 degrees or more! Until we put our LED/sensor combo inside a container, they could receive IR from the LED no matter which angle they were facing. But just being able to consistently receive IR from the object was only half the problem - we also wanted to ensure that both the base and the object were in sync (something that wasn't always possible with just single-way communication)
Using a send and receive pair in both the object and the base, we modified the firmware slightly, to use a call-and-response method of keeping the two in sync.
Originally we just had the object report it's status and the base would update to indicate to the user the current state of the object. But sometimes the object would change state and the base failed to receive the updated status. The two would often go out of sync.
With this new hardware, the messaging process is going to be:
a) the object detects a change
b) the object sends a message to the base
c) the base updates its' display (LEDs)
d) the base returns the message to the object (up to three times)
e) the object updates its status when it receives any one of these messages.
This method of send and receive ensures that if the message from the object doesn't get to the base (for whatever reason) the two remain in sync. It's almost like the object is asking "can I go to state x" and the base confirms "yes, you can now move to state x". Only when the response message is received does does the object update its status. This should ensure that the display on the base matches the status of the object that initiated the change.
Of course it's not 100% foolproof - there's a chance that the message from the base to the object gets lost and they go out of sync again. But it's less likely to happen because the response message is sent almost instantly (and is repeated up to three times). So as soon as the object sends its "request" message, the base responds (within about 50ms). As long as the object hasn't moved too much within this tiny time frame, it should be able to receive at least one of the three response messages.
Here's how we set up our two Arduinos:
And the firmware (the same firmware is uploaded to both and the state of the button pin checked on boot up to decide the "role" of the device- either base or object).
#include <IRLib.h>
//Create a receiver object to listen on pin 10
IRrecv My_Receiver(10);
IRsend My_Sender;
IRdecode My_Decoder;
#define IR_RECEIVE_ALL_THE_TIME 0
#define IR_SEND_THEN_RECEIVE 1
#define IR_RECEIVE_FOR_1_SEC 2
#define IR_SEND_RELAY 3
int button_pin=12;
bool is_sender = true;
int ir_state = IR_RECEIVE_ALL_THE_TIME;
long ir_value = 0xf578;
long ir_last_value = 0x0000;
long time_start_listening;
long time_now;
long time_diff;
void sendIRValue(){
Serial.print(F("Sending: "));
Serial.println(ir_value, HEX);
My_Sender.send(RC5, ir_value, 13);
delay(10);
My_Sender.send(RC5, ir_value, 13);
}
void parseMessage(){
// now what to do with the received IR value?
Serial.print(F("Parsing: "));
Serial.println(ir_value, HEX);
// for testing the base, just send this message back out
// (for the sender, don't do anything)
if(is_sender==false){
delay(2);
ir_state = IR_SEND_RELAY;
}else{
if(ir_value == ir_last_value){
Serial.println(F("Nothing to do, repeat value received"));
}else{
Serial.println(F("Change language"));
}
}
ir_last_value = ir_value;
}
void setup() {
Serial.begin(19200);
pinMode(button_pin, INPUT_PULLUP);
My_Receiver.enableIRIn(); // Start the receiver
// this is just for testing
int b = digitalRead(button_pin);
if(b == LOW){
is_sender=false;
Serial.println(F("I am the base"));
}else{
Serial.println(F("I am the sender"));
}
}
void loop() {
int b;
time_now = millis();
// this is just for testing
b = digitalRead(button_pin);
if(b==LOW && is_sender==true){
// debounce the button press
delay(10);
b = digitalRead(button_pin);
if(b==LOW){
// wait for release
while(b==LOW){ b = digitalRead(button_pin); }
// now send a blast of IR
ir_state = IR_SEND_THEN_RECEIVE;
}
}
// deal with any incoming IR (if necessary)
switch(ir_state){
//-----------------------------
case IR_RECEIVE_ALL_THE_TIME:
//-----------------------------
if (My_Receiver.GetResults(&My_Decoder)) {
My_Decoder.decode(); //Decode the data
b = My_Decoder.value & 0x1fff; // our data is only 13 bits long
if(b > 0){
ir_value = b;
parseMessage();
}
My_Receiver.resume(); //Restart the receiver
}
break;
//-----------------------------
case IR_SEND_THEN_RECEIVE:
//-----------------------------
delay(20);
sendIRValue();
// now we listen for a response for a second or two
time_start_listening = millis();
// re-enable the receiver
My_Receiver.enableIRIn();
ir_state = IR_RECEIVE_FOR_1_SEC;
break;
//-----------------------------
case IR_SEND_RELAY:
//-----------------------------
delay(50);
sendIRValue();
// send the response message another couple of
// times in case one of them burps.
delay(50);
sendIRValue();
delay(50);
sendIRValue();
// re-enable the receiver
My_Receiver.enableIRIn();
ir_state = IR_RECEIVE_ALL_THE_TIME;
break;
//-----------------------------------
case IR_RECEIVE_FOR_1_SEC:
//-----------------------------------
if (My_Receiver.GetResults(&My_Decoder)) {
My_Decoder.decode(); //Decode the data
b = My_Decoder.value & 0x1fff; // our data is only 13 bits long
if(b > 0){
ir_value = b;
parseMessage();
// now leave this state
ir_state = 99;
}
My_Receiver.resume(); //Restart the receiver
}
time_diff = time_now - time_start_listening;
if(time_diff > 500){
// leave this state
Serial.println(F("Leaving receive state"));
ir_state = 99;
}
break;
}
}
Because the object can spin, rotate and generally move around, we had to implement a wireless method of sending information between the base and the object. Radio seemed a bit overkill (and, since the object is battery powered, we wanted to reduce the current draw as much as possible). IR seemed like the perfect solution.
In fact, we had a working solution a few weeks back, using tiny reflective sensors. But instead of sending a signal and reading the reflected response, both the object and the base had a QRE1113 sensor inside them - so the base didn't read it's own reflected signal, it received a signal sent from the object.
Everything seemed fine. Except the distance between the sensors was critical. At this distance, everything worked great.
But at this distance...
... even after ensuring that the sensors are perfectly in line, the signal often didn't make it from one to the other. After inspecting the IR LED in the sensor (using a phone camera) the amount of light emitted appears as a tiny, tiny dot. Basically, these reflective sensors are good up to about 8-10mm. Anything more than that and the signal gets lost - or occasionally corrupted.
We wanted to increase the distance between our sensors so needed something with a bit more oomph. A simple IR LED and IR phototransistor seemed like a good idea.
Except these were very sensitive to the angle of two being absolutely accurate. Even just a couple of degrees off and the phototransistor stopped responding to the IR LED.
So Steve suggested using the Vishay 38khz IR receivers, most commonly found in TV units (to decode remote control button presses). These are able to receive IR bounced around from all over the place!
Not only do they work if you point an LED directly at a sensor....
They also work at greater distances, and even at 90 degrees or more! Until we put our LED/sensor combo inside a container, they could receive IR from the LED no matter which angle they were facing. But just being able to consistently receive IR from the object was only half the problem - we also wanted to ensure that both the base and the object were in sync (something that wasn't always possible with just single-way communication)
Using a send and receive pair in both the object and the base, we modified the firmware slightly, to use a call-and-response method of keeping the two in sync.
Originally we just had the object report it's status and the base would update to indicate to the user the current state of the object. But sometimes the object would change state and the base failed to receive the updated status. The two would often go out of sync.
With this new hardware, the messaging process is going to be:
a) the object detects a change
b) the object sends a message to the base
c) the base updates its' display (LEDs)
d) the base returns the message to the object (up to three times)
e) the object updates its status when it receives any one of these messages.
This method of send and receive ensures that if the message from the object doesn't get to the base (for whatever reason) the two remain in sync. It's almost like the object is asking "can I go to state x" and the base confirms "yes, you can now move to state x". Only when the response message is received does does the object update its status. This should ensure that the display on the base matches the status of the object that initiated the change.
Of course it's not 100% foolproof - there's a chance that the message from the base to the object gets lost and they go out of sync again. But it's less likely to happen because the response message is sent almost instantly (and is repeated up to three times). So as soon as the object sends its "request" message, the base responds (within about 50ms). As long as the object hasn't moved too much within this tiny time frame, it should be able to receive at least one of the three response messages.
Here's how we set up our two Arduinos:
Send Receive 38khz by chris_holden2495 on Scribd
And the firmware (the same firmware is uploaded to both and the state of the button pin checked on boot up to decide the "role" of the device- either base or object).
#include <IRLib.h>
//Create a receiver object to listen on pin 10
IRrecv My_Receiver(10);
IRsend My_Sender;
IRdecode My_Decoder;
#define IR_RECEIVE_ALL_THE_TIME 0
#define IR_SEND_THEN_RECEIVE 1
#define IR_RECEIVE_FOR_1_SEC 2
#define IR_SEND_RELAY 3
int button_pin=12;
bool is_sender = true;
int ir_state = IR_RECEIVE_ALL_THE_TIME;
long ir_value = 0xf578;
long ir_last_value = 0x0000;
long time_start_listening;
long time_now;
long time_diff;
void sendIRValue(){
Serial.print(F("Sending: "));
Serial.println(ir_value, HEX);
My_Sender.send(RC5, ir_value, 13);
delay(10);
My_Sender.send(RC5, ir_value, 13);
}
void parseMessage(){
// now what to do with the received IR value?
Serial.print(F("Parsing: "));
Serial.println(ir_value, HEX);
// for testing the base, just send this message back out
// (for the sender, don't do anything)
if(is_sender==false){
delay(2);
ir_state = IR_SEND_RELAY;
}else{
if(ir_value == ir_last_value){
Serial.println(F("Nothing to do, repeat value received"));
}else{
Serial.println(F("Change language"));
}
}
ir_last_value = ir_value;
}
void setup() {
Serial.begin(19200);
pinMode(button_pin, INPUT_PULLUP);
My_Receiver.enableIRIn(); // Start the receiver
// this is just for testing
int b = digitalRead(button_pin);
if(b == LOW){
is_sender=false;
Serial.println(F("I am the base"));
}else{
Serial.println(F("I am the sender"));
}
}
void loop() {
int b;
time_now = millis();
// this is just for testing
b = digitalRead(button_pin);
if(b==LOW && is_sender==true){
// debounce the button press
delay(10);
b = digitalRead(button_pin);
if(b==LOW){
// wait for release
while(b==LOW){ b = digitalRead(button_pin); }
// now send a blast of IR
ir_state = IR_SEND_THEN_RECEIVE;
}
}
// deal with any incoming IR (if necessary)
switch(ir_state){
//-----------------------------
case IR_RECEIVE_ALL_THE_TIME:
//-----------------------------
if (My_Receiver.GetResults(&My_Decoder)) {
My_Decoder.decode(); //Decode the data
b = My_Decoder.value & 0x1fff; // our data is only 13 bits long
if(b > 0){
ir_value = b;
parseMessage();
}
My_Receiver.resume(); //Restart the receiver
}
break;
//-----------------------------
case IR_SEND_THEN_RECEIVE:
//-----------------------------
delay(20);
sendIRValue();
// now we listen for a response for a second or two
time_start_listening = millis();
// re-enable the receiver
My_Receiver.enableIRIn();
ir_state = IR_RECEIVE_FOR_1_SEC;
break;
//-----------------------------
case IR_SEND_RELAY:
//-----------------------------
delay(50);
sendIRValue();
// send the response message another couple of
// times in case one of them burps.
delay(50);
sendIRValue();
delay(50);
sendIRValue();
// re-enable the receiver
My_Receiver.enableIRIn();
ir_state = IR_RECEIVE_ALL_THE_TIME;
break;
//-----------------------------------
case IR_RECEIVE_FOR_1_SEC:
//-----------------------------------
if (My_Receiver.GetResults(&My_Decoder)) {
My_Decoder.decode(); //Decode the data
b = My_Decoder.value & 0x1fff; // our data is only 13 bits long
if(b > 0){
ir_value = b;
parseMessage();
// now leave this state
ir_state = 99;
}
My_Receiver.resume(); //Restart the receiver
}
time_diff = time_now - time_start_listening;
if(time_diff > 500){
// leave this state
Serial.println(F("Leaving receive state"));
ir_state = 99;
}
break;
}
}
Sunday, 16 October 2016
More matched betting
It turns out it was about a year ago (almost to the day) that a few of us had a go at Matched Betting. In fact, Matt had been doing it for a while before then, and it was only when he was showing off his £2k new PC with uber-graphics cards that we were convinced it wasn't all going to go horribly wrong and end up with him living in a cardboard box under a railway bridge.
But, sure enough, he knew what he was doing. And seemed to be making money quite easily. I had a go and, following his instructions, I made about £700 over two months. I stopped after doing all the "easy" sign-up offers (rather than continue with the reload offers) but this left me with £700 tied up in a couple of betting exchange accounts.
Rather than withdraw this cash, I did the same offers again, on behalf of friends and family. This time, having a large(ish) amount in the exchanges allowed me to do multiple offers at once, getting through the offers more quickly and turning free bets into cash. I turned over about £450 within ten days for my mother, £400 within about the same timeframe for one of my friends in Brighton, and about £300 for another friend, just in time for Xmas.
It's worth noting that as Matched Betting became "a thing" late last year, and as more and more of us were doing it, the rewards for the time invested went down and down - the bookies were onto it, pretty quickly!
However, having Christmas dinner for the entire family paid for by Mssrs Ladbrokes, Paddy Power and William Hill was quite a nice touch. So, naturally, we're going to give it another try this year. Now, of course, we can't go doing the same sign-up offers again, but - in preparation for running a few matched bets through the exchanges again this year- I was clearing up some old accounts, and in doing so was offered a couple of free bets to get an otherwise dormant account active again. It's thanks to this that I found a bit of an anomaly:
Betfair (the exchange we use to "lay" the bets off) was offering a £25 free sportsbook bet. Simply put, if I wagered £25 on a an event (I choose Liverpool to beat Man U on Monday) we were immediately given a £25 free bet. I did exactly this, then went to the Smarkets website and laid off Liverpool (i.e bet on United to win or draw) for the same value.
Because each exchange charges a percentage commission, this "matched bet" was always going to lose a little bit of money. It turned out that the combination of prices chosenm, I stood to lose £1.69 irrespective of the result of the Liverpool v ManU game. A kick in the pants. But worth it, to earn a free £25 bet.
So with the "qualifying bet" placed and matched on the exchange, I was pretty pleased to see the free bet immediately appear in the Betfair account. Now I'm already having to wait over 24 hours for the qualifying bet to resolve, and didn't want to wait longer than this again, so chose to place the free bet on a much higher odds result on a football game happening in just a couple of hours time.
I chose Villareal and bet that their opponents (at 5/1) would win. The game was to kick off just two hours after the bet was placed, so it would all by over in a little under four hours. Of course, my bet selection didn't win (Villareal won 4-nil in the end) so our free Betfair sportsbook bet lost, and the amount in our Smarkets account went up.
So I just had to wait for the Liverpool/Man U match to resolve, and I'd have turned the free bet into cash. Except I didn't actually have to wait. Thanks to the "cash in" feature on Betfair, I was able to "cancel" the qualifying bet:
And by betting on the reverse result on the Smarket exchange, effectively "cancel" that bet too.
And if we look at the costs involved:
To cash out the Betfair bet, they took £24.31 from our initial £25 bet - so it cost us £0.69. Irrespective of the result on Smarkets (whether Liverpool win, Man U win or it's a draw) we lose £0.47 at most (only £0.42 if it's anything other than a Liverpool win). Which means that, irrespective of the match result, we've lost a total of £1.16 in "cancelling" our qualifying bets.
But this is less than the £1.69 it would have cost us, if we'd let the bets stand, to resolve fully 24 hours later. In short, it worked out better for placing a qualifying bet on an event that occurred after the event I had used the free bet on.
Once the free bet was "spent" (i.e. it lost and the money appeared in the Smarkets betting exchange - after all, the bet is only really insurance in case the lay bet fails) I was able to "cancel" the qualifying bet at Betfair, and the lay bet on Smarkets. Not only to generate lower loss, but also to resolve the entire position 24 hours earlier than had I allowed the bets to run to their full conclusion.
It's a tactic I might look at again in the future.
And one that everyone else doing Matched Betting might want to consider too. After all, if you're trying to turn over a number of high-value bets quickly, getting the cash out of the bookies and back into the exchange balance, rather than have it riding on sports events is critical. Being able to cancel qualifying bets and get the money back 24 hours earlier means another day of opportunities tomorrow - rather than having to pass over them because of a lack of cashflow!
There are a few of use having a go at matched betting over the coming weeks. We might just post some results here, to log our progress and - hopefully - explain how it all works a little bit better (complete with cock-ups, mis-calculations and lucky breaks).
The aim is a rather modest £300 for Christmas. At least it means no-one has to worry about paying for a turkey big enough to feed 14 of us on the big day!
But, sure enough, he knew what he was doing. And seemed to be making money quite easily. I had a go and, following his instructions, I made about £700 over two months. I stopped after doing all the "easy" sign-up offers (rather than continue with the reload offers) but this left me with £700 tied up in a couple of betting exchange accounts.
Rather than withdraw this cash, I did the same offers again, on behalf of friends and family. This time, having a large(ish) amount in the exchanges allowed me to do multiple offers at once, getting through the offers more quickly and turning free bets into cash. I turned over about £450 within ten days for my mother, £400 within about the same timeframe for one of my friends in Brighton, and about £300 for another friend, just in time for Xmas.
It's worth noting that as Matched Betting became "a thing" late last year, and as more and more of us were doing it, the rewards for the time invested went down and down - the bookies were onto it, pretty quickly!
However, having Christmas dinner for the entire family paid for by Mssrs Ladbrokes, Paddy Power and William Hill was quite a nice touch. So, naturally, we're going to give it another try this year. Now, of course, we can't go doing the same sign-up offers again, but - in preparation for running a few matched bets through the exchanges again this year- I was clearing up some old accounts, and in doing so was offered a couple of free bets to get an otherwise dormant account active again. It's thanks to this that I found a bit of an anomaly:
Betfair (the exchange we use to "lay" the bets off) was offering a £25 free sportsbook bet. Simply put, if I wagered £25 on a an event (I choose Liverpool to beat Man U on Monday) we were immediately given a £25 free bet. I did exactly this, then went to the Smarkets website and laid off Liverpool (i.e bet on United to win or draw) for the same value.
Because each exchange charges a percentage commission, this "matched bet" was always going to lose a little bit of money. It turned out that the combination of prices chosenm, I stood to lose £1.69 irrespective of the result of the Liverpool v ManU game. A kick in the pants. But worth it, to earn a free £25 bet.
So with the "qualifying bet" placed and matched on the exchange, I was pretty pleased to see the free bet immediately appear in the Betfair account. Now I'm already having to wait over 24 hours for the qualifying bet to resolve, and didn't want to wait longer than this again, so chose to place the free bet on a much higher odds result on a football game happening in just a couple of hours time.
I chose Villareal and bet that their opponents (at 5/1) would win. The game was to kick off just two hours after the bet was placed, so it would all by over in a little under four hours. Of course, my bet selection didn't win (Villareal won 4-nil in the end) so our free Betfair sportsbook bet lost, and the amount in our Smarkets account went up.
So I just had to wait for the Liverpool/Man U match to resolve, and I'd have turned the free bet into cash. Except I didn't actually have to wait. Thanks to the "cash in" feature on Betfair, I was able to "cancel" the qualifying bet:
And by betting on the reverse result on the Smarket exchange, effectively "cancel" that bet too.
And if we look at the costs involved:
To cash out the Betfair bet, they took £24.31 from our initial £25 bet - so it cost us £0.69. Irrespective of the result on Smarkets (whether Liverpool win, Man U win or it's a draw) we lose £0.47 at most (only £0.42 if it's anything other than a Liverpool win). Which means that, irrespective of the match result, we've lost a total of £1.16 in "cancelling" our qualifying bets.
But this is less than the £1.69 it would have cost us, if we'd let the bets stand, to resolve fully 24 hours later. In short, it worked out better for placing a qualifying bet on an event that occurred after the event I had used the free bet on.
Once the free bet was "spent" (i.e. it lost and the money appeared in the Smarkets betting exchange - after all, the bet is only really insurance in case the lay bet fails) I was able to "cancel" the qualifying bet at Betfair, and the lay bet on Smarkets. Not only to generate lower loss, but also to resolve the entire position 24 hours earlier than had I allowed the bets to run to their full conclusion.
It's a tactic I might look at again in the future.
And one that everyone else doing Matched Betting might want to consider too. After all, if you're trying to turn over a number of high-value bets quickly, getting the cash out of the bookies and back into the exchange balance, rather than have it riding on sports events is critical. Being able to cancel qualifying bets and get the money back 24 hours earlier means another day of opportunities tomorrow - rather than having to pass over them because of a lack of cashflow!
There are a few of use having a go at matched betting over the coming weeks. We might just post some results here, to log our progress and - hopefully - explain how it all works a little bit better (complete with cock-ups, mis-calculations and lucky breaks).
The aim is a rather modest £300 for Christmas. At least it means no-one has to worry about paying for a turkey big enough to feed 14 of us on the big day!
Wednesday, 12 October 2016
Messing about with MAX7219 common anode 8x8 LED matrix and Arduino
Many years ago, we built a couple of word clocks. They were pretty good fun and we used both 74HC595 shift registers and the MAX7219 LED driver chip. As we've been working with Arduino so much in recent months, we figured it'd be interesting to port some earlier code to create a working prototype.
So here it is:
Firstly, we had no information on our LED matrix. It was in a drawer somewhere so we had to work out the connections. This involved nothing more than taking a 3v lead and a ground lead and touching them to each of the pins on the matrix, to see which LEDs lit up.
As each LED lit up, we noted which pin was grounded and which had power, and from this we were able to calculate which pin was for which row and which pin was for which column.
Once we had worked out the rows and columns on the 8x8 matrix, it was a case of hooking up a few wires and giving it a try!
According to the MAX7219 datasheet, turning LEDs on and off involves writing data to memory addresses using a two-byte 16-bit value. Luckily for us, it's a relatively simple protocol.
The first byte is the memory address - or row - to write the "image data" to. Memory address 0x01 corresponds to row 1, 0x02 to row 2 etc.
The second byte contains the LED on/off data in binary format, where 1 = LED on and 0 = LED off. So if we wanted to switch on just the first two LEDs in row 3, we would send the two byte message
0x03, 0xC0 (where 0xC0 is binary 11000000)
So we built some simple bit-banging routines (no nasty libraries here!) to pump data into the MAX7219 chip.
#define data_pin 7
#define clock_pin 8
#define load_pin 9
#define led_pin 13
bool led = false;
void sendData(int k){
int j;
Serial.print(F("sending: "));
digitalWrite(load_pin,LOW);
for(int i=0; i<=15; i++){
j = k & 0x8000;
if(j==0){
digitalWrite(data_pin,LOW);
Serial.print("0");
}else{
digitalWrite(data_pin,HIGH);
Serial.print("1");
}
digitalWrite(clock_pin,HIGH);
delayMicroseconds(20);
digitalWrite(clock_pin,LOW);
k = k << 1;
}
Serial.println("");
toggleLoadPin();
}
void toggleLoadPin(){
digitalWrite(load_pin,HIGH);
delay(1);
digitalWrite(load_pin,LOW);
}
void sendValue(int ad, int v){
int k=0;
k=ad;
k=k<<8;
k=k|v;
sendData(k);
}
void setup() {
// put your setup code here, to run once:
pinMode(data_pin,OUTPUT);
pinMode(clock_pin,OUTPUT);
pinMode(load_pin,OUTPUT);
Serial.begin(19200);
// this turns off the LEDs on the matrix
// (though we can still send data to the max7219)
// lc.shutdown(0,false);// turn off power saving, enables display
// lc.setIntensity(0,15);// sets brightness (0~15 possible values)
// lc.clearDisplay(0);// clear screen
int k=0;
int addr;
int val;
// no decode mode
sendValue(0x09, 0x00); // do not use BCD
// full brightness
sendValue(0x0A, 0x0F);
// scan limit = 8 (use 8 rows)
sendValue(0x0B, 0x07);
// normal operation
sendValue(0x0C, 0x01); // display on
sendValue(0x0F, 0x00); // no test mode
// display test
sendValue(0x0F, 0x01);
delay(1500);
sendValue(0x0F, 0x00);
// now light up all rows
sendValue(0x01,0x55);
sendValue(0x02,0xAA);
sendValue(0x03,0x0F);
sendValue(0x04,0xF0);
sendValue(0x05,0x00);
sendValue(0x06,0xFF);
sendValue(0x07,0x3C);
sendValue(0x08,0xC3);
}
void loop(){
// put your main code here, to run repeatedly:
}
To pump data onto the LED display, simply call the function SENDVALUE (row, led_data)
So here it is:
Firstly, we had no information on our LED matrix. It was in a drawer somewhere so we had to work out the connections. This involved nothing more than taking a 3v lead and a ground lead and touching them to each of the pins on the matrix, to see which LEDs lit up.
As each LED lit up, we noted which pin was grounded and which had power, and from this we were able to calculate which pin was for which row and which pin was for which column.
Once we had worked out the rows and columns on the 8x8 matrix, it was a case of hooking up a few wires and giving it a try!
According to the MAX7219 datasheet, turning LEDs on and off involves writing data to memory addresses using a two-byte 16-bit value. Luckily for us, it's a relatively simple protocol.
The first byte is the memory address - or row - to write the "image data" to. Memory address 0x01 corresponds to row 1, 0x02 to row 2 etc.
The second byte contains the LED on/off data in binary format, where 1 = LED on and 0 = LED off. So if we wanted to switch on just the first two LEDs in row 3, we would send the two byte message
0x03, 0xC0 (where 0xC0 is binary 11000000)
So we built some simple bit-banging routines (no nasty libraries here!) to pump data into the MAX7219 chip.
Max 7219 by chris_holden2495 on Scribd
#define data_pin 7
#define clock_pin 8
#define load_pin 9
#define led_pin 13
bool led = false;
void sendData(int k){
int j;
Serial.print(F("sending: "));
digitalWrite(load_pin,LOW);
for(int i=0; i<=15; i++){
j = k & 0x8000;
if(j==0){
digitalWrite(data_pin,LOW);
Serial.print("0");
}else{
digitalWrite(data_pin,HIGH);
Serial.print("1");
}
digitalWrite(clock_pin,HIGH);
delayMicroseconds(20);
digitalWrite(clock_pin,LOW);
k = k << 1;
}
Serial.println("");
toggleLoadPin();
}
void toggleLoadPin(){
digitalWrite(load_pin,HIGH);
delay(1);
digitalWrite(load_pin,LOW);
}
void sendValue(int ad, int v){
int k=0;
k=ad;
k=k<<8;
k=k|v;
sendData(k);
}
void setup() {
// put your setup code here, to run once:
pinMode(data_pin,OUTPUT);
pinMode(clock_pin,OUTPUT);
pinMode(load_pin,OUTPUT);
Serial.begin(19200);
// this turns off the LEDs on the matrix
// (though we can still send data to the max7219)
// lc.shutdown(0,false);// turn off power saving, enables display
// lc.setIntensity(0,15);// sets brightness (0~15 possible values)
// lc.clearDisplay(0);// clear screen
int k=0;
int addr;
int val;
// no decode mode
sendValue(0x09, 0x00); // do not use BCD
// full brightness
sendValue(0x0A, 0x0F);
// scan limit = 8 (use 8 rows)
sendValue(0x0B, 0x07);
// normal operation
sendValue(0x0C, 0x01); // display on
sendValue(0x0F, 0x00); // no test mode
// display test
sendValue(0x0F, 0x01);
delay(1500);
sendValue(0x0F, 0x00);
// now light up all rows
sendValue(0x01,0x55);
sendValue(0x02,0xAA);
sendValue(0x03,0x0F);
sendValue(0x04,0xF0);
sendValue(0x05,0x00);
sendValue(0x06,0xFF);
sendValue(0x07,0x3C);
sendValue(0x08,0xC3);
}
void loop(){
// put your main code here, to run repeatedly:
}
To pump data onto the LED display, simply call the function SENDVALUE (row, led_data)
Sunday, 9 October 2016
Reading data from a web page using Arduino and an ESP8266 module (no esp8266.h library)
A little while back, we were playing about with our newly-updated ESP8266 modules, with version 0.9.4 firmware. They're great little modules, and make excellent socket-based clients. But our new firmware broke the esp8266 Arduino library. Which meant we had to roll our own code for sending, receiving and parsing AT commands.
The basics are pretty simple - chuck a few strings into a few functions, send AT commands, catch the responses as strings and use the usual built-in parsing functions.
But when you start downloading entire web page contents (or, tbh, anything more than a couple of hundred bytes at a time) the responses get a little... well, confused it putting it mildly.
Now no-end of people will say "avoid Strings on an embedded controller". I'm one of them. But this was as much an exercise in making shareable Arduino code as much as it was about making a working solution. So while the very first impulse, when things started to go wrong, was "sod this, let's shove it on a PIC and use byte arrays", we had to resist - and shove it on an Arduino and use byte arrays....
There's a bit of confusion between Strings and byte/char arrays in Arduino-land. We haven't the scope to cover C++ programming concepts, pointers and memory locations here. But there's probably just about enough space to throw down some (working) code from which you can build your own embedded, web-based projects.
So let's get down to it....
The thing to remember is that we're working on an embedded system. An mcu with a massive 2k of memory. That's not a lot. So even though we're sending data to and from a web site, we need to write our web pages so as to keep the total character count down to a minimum.
We've built our input buffer to be about 600 characters. We might get away with a few hundred more. 1024 characters at push - the ESP8266 "chunks" large responses into 1024 bytes anyway. But that doesn't leave a massive amount of RAM left for all the other stuff we need to do. And it only takes one string of data to exceed the expected length, and we get all kinds of weird bugs and faults and corrupted data.
So although this code can retrieve data from a web page, it does so on the understanding that the web page itself is very very lightweight.
#include <LiquidCrystal.h>
#include <EEPROM.h>
#define wifi_reset 10
#define led_pin 13
#define button1_pin 12
#include <SoftwareSerial.h>
SoftwareSerial mySerial(8, 9); // RX, TX
// ------------ change this stuff --------------
String ssid = "YOUR_SSID";
String pwd = "YOUR_PWD";
String user = "Chris";
#define DEST_IP "188.226.228.99"
#define HTTP_REQ " HTTP/1.1\r\nHost: www.yourdomain.co.uk:80\r\n\r\n"
#define HTML_TIMEOUT 10
#define HTML_OVERHEAD 2
bool in_debug = true;
bool show_wifi_output = false;
// ---------------------------------------------
int state=0;
bool has_ip = false;
char b;
String cmd;
bool wifi_matched = false;
char wifi_response[500];
char *ptr;
int ptr_index;
String this_ip;
void emptyWifiResponse(){
memset(wifi_response, 0, sizeof(wifi_response));
ptr_index=0;
}
void writeAT(String at){
emptyWifiResponse();
if(in_debug==true){
Serial.print(F("Sending: "));
Serial.println(at);
}
// empty the incoming serial buffer
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
mySerial.println(at);
}
bool wifiResponseContainsHeader(){
bool b=false;
int k=0;
if(wifi_response[ptr_index-14]==':'){ k++;}
if(wifi_response[ptr_index-13]==' '){ k++;}
if(wifi_response[ptr_index-12]=='t'){ k++;}
if(wifi_response[ptr_index-11]=='e'){ k++;}
if(wifi_response[ptr_index-10]=='x'){ k++;}
if(wifi_response[ptr_index- 9]=='t'){ k++;}
if(wifi_response[ptr_index- 8]=='/'){ k++;}
if(wifi_response[ptr_index- 7]=='h'){ k++;}
if(wifi_response[ptr_index- 6]=='t'){ k++;}
if(wifi_response[ptr_index- 5]=='m'){ k++;}
if(wifi_response[ptr_index- 4]=='l'){ k++;}
// characters -3, -2, -1, 0 are the
// combination of CrLf x 2
if(k>=9){ b=true; }
return(b);
}
bool wifiResponseContains(String findThis){
// search the entire character array for the string
char __findThis[sizeof(findThis)];
findThis.toCharArray(__findThis, sizeof(__findThis));
ptr=strstr(wifi_response, __findThis);
if(ptr!=NULL){
return(true);
}else{
return(false);
}
}
void wifiResponse_Remove(String findThis){
// find a particular string in the char array and remove it
// (reducing the size of the array as necessary)
// we could use clever C functions like memmove etc. but for some
// functions behaviour is undefined if strings overlap, and for some
// they create a shadow copy before moving; we might not have the
// memory available to do this, so lets use the array itself
// get the index of the first character in the string
// we're trying to remove
char c;
int c_index=0;
int r_index=-1;
int match_count = 0;
int find_count = findThis.length();
int buff_len = 500-find_count;
char __findThis[find_count+1];
findThis.toCharArray(__findThis, find_count+1);
// walk through the response array, looking for the search string
for(int i=0; i<500; i++){
if(wifi_response[i]==__findThis[c_index]){
match_count++;
c_index++;
if(match_count==find_count){
// we've just found our string
r_index=(i-find_count)+1;
// quit the for-next loop early
i=500;
}
}else{
match_count=0;
c_index=0;
}
}
if(r_index > -1){
// remove x characters from the array by simply budging everything up
for(int i=r_index; i<buff_len; i++){
wifi_response[i] = wifi_response[i+find_count];
}
for(int i=buff_len+1; i<500; i++){ wifi_response[i]=0; }
}
}
bool checkIP(){
// first, send a CIFSR instruction then wait for a response
this_ip="0.0.0.0";
writeAT(F("AT+CIFSR"));
readInput(4000, "192.168.", "0.0.0.0", true);
if(wifi_matched==true){
if(wifiResponseContains("OK")==true){
wifiResponse_Remove("OK");
wifiResponse_Remove("AT+CIFSR");
// convert the char array into a string
// this_ip=wifi_response;
this_ip=wifi_response;
return(true);
}else{
return(false);
}
}else{
return(false);
}
}
void readInput(int timeout, String match, String err, bool exitOnFind) {
unsigned long stop = millis()+timeout;
emptyWifiResponse();
wifi_matched=false;
if(in_debug==true){
Serial.print(F("Looking for: "));
Serial.println(match);
}
if(show_wifi_output==true){
Serial.print(F("raw wifi >"));
}else{
Serial.print(F("wifi response: "));
}
// point to the start of the character array
ptr_index=0;
do {
while(mySerial.available()>0){
char b=mySerial.read();
if(show_wifi_output==true){ Serial.print(b);}
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
if(wifiResponseContains(err)==true){
// error condition has been matched
if(in_debug==true){ Serial.println(F("Error condition matched")); }
stop=millis()-1;
// read the rest of the buffer just to empty it
emptyWifiResponse();
while(mySerial.available()>0){
b = mySerial.read();
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
}
break;
}
if(wifiResponseContains(match) == true ){
// requested string has been matched
// so now return everything after the matched word
if(in_debug==true){
Serial.println(F("Match condition found"));
Serial.println();
}
wifi_matched = true;
if(exitOnFind==true){
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){
b = mySerial.read();
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
}
break;
}
}
}
} while (millis() < stop);
// this is a bit hacky, but it'll have to do.
// sometimes, when we're expecting OK we get "no change"
// but if there's been no change, that's the same as
// OK, so let's deal with that
if(show_wifi_output==true){
Serial.println(F("<"));
Serial.print(F("Buffered response: "));
}
if(in_debug==true){ Serial.println(wifi_response); }
if(match=="OK" && wifiResponseContains("no change")==true){
if(in_debug==true){ Serial.print(F(" OK/no change (close enough) ")); }
wifi_matched=true;
}
if(in_debug==true){
Serial.print(F("match found: "));
Serial.println(wifi_matched);
}
}
void readCloseInput(int timeout, String match, String err, bool exitOnFind) {
unsigned long stop = millis()+timeout;
cmd="";
bool closeOK = false;
// because the response from an AT command is small, we're using this
// simplified function (complete with Strings!) to check the response
// to the CIPCLOSE command, without wiping out the character array
// buffer (which at this point contains the HTML we want to parse)
do {
while(mySerial.available()>0){
char b=mySerial.read();
if(show_wifi_output==true){ Serial.print(b);}
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
cmd.concat(b);
}
if(cmd.indexOf(err)>=0){
// error condition has been matched
if(in_debug==true){ Serial.println(F("Error condition matched")); }
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){ b = mySerial.read(); }
break;
}
if(cmd.indexOf(match)>=0 ){
// requested string has been matched
// so now return everything after the matched word
if(in_debug==true){ Serial.println(F("Match condition found")); }
closeOK = true;
if(exitOnFind==true){
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){ b = mySerial.read(); }
break;
}
}
}
} while (millis() < stop);
if(in_debug==true){
Serial.print(F("Closed ok: "));
Serial.println(closeOK);
}
}
void flashLED(int k){
for(int i=0; i<k; i++){
digitalWrite(led_pin,HIGH);
delay(250);
digitalWrite(led_pin,LOW);
delay(500);
}
}
void connectToRouter(){
String p;
// hardware reset the module
if(in_debug==true){ Serial.println(F("Resetting wifi"));}
digitalWrite(wifi_reset, LOW);
delay(500);
digitalWrite(wifi_reset, HIGH);
// Look for ready string from wifi module
readInput(4000, "Ready", "Error", true);
if(in_debug==true){
Serial.print(F("Wifi read: "));
Serial.println(wifi_response);
}
if(wifi_matched==true){
writeAT(F("AT+CWMODE=1"));
readInput(2500, "OK", "Error", true);
if(in_debug==true){
Serial.print(F("Wifi: "));
Serial.println(wifi_response);
}
if(wifi_matched==true || wifi_matched!=true){
delay(1000);
has_ip=checkIP();
if(has_ip==false){
p = "AT+CWJAP=\"";
p.concat(ssid);
p.concat("\",\"");
p.concat(pwd);
p.concat("\"");
writeAT(p);
// there's not really an OK message with CWJAP
// just returns a string when completed
readInput(16000, "OK", "FAIL", true);
if(wifiResponseContains("FAIL")==true){
// can't connect to the access point
Serial.println(F("Can't get onto the access point"));
}else if(wifi_matched==true){
// so now query the system for an IP address again
has_ip=checkIP();
if(has_ip==true){
Serial.print(F("This ip address is: "));
Serial.println(this_ip);
}else{
// access point refused
Serial.println(F("Refused access to AP"));
}
}else{
Serial.println(F("No response from AP"));
}
}
}
}else{
Serial.println(F("Unable to reboot wifi module"));
}
}
void getWebPageContents(String url){
cmd="AT+CIPSTART=\"TCP\",\"";
cmd.concat(DEST_IP);
cmd.concat("\",80");
writeAT(cmd);
readInput(8000, "Linked", "ERROR", true);
if(wifi_matched==true){
// tell the server how many bytes to expect
// by first building the request then getting
// a character count (including trailing \r\n)
delay(100);
cmd = "GET ";
cmd += url;
cmd += "?user=";
cmd += user;
cmd += HTTP_REQ;
String c = "AT+CIPSEND=";
c.concat(String(cmd.length()));
writeAT(c);
readInput(4000, ">", "Error", true);
if(wifi_matched==true){
// now send the request to actually get some data
delay(500);
emptyWifiResponse();
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
if(in_debug==true){
Serial.print(F("Sending: "));
Serial.println(cmd);
}
mySerial.print(cmd);
// now we can't just use our readInput as the response could be in multipart chunks
// so we need to parse this separately
getHTMLResponse();
}else{
Serial.println(F("Can't enter send data mode"));
emptyWifiResponse();
}
// now close the connection
delay(300);
// empty the incoming serial buffer
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
mySerial.println(F("AT+CIPCLOSE"));
// while it's tempting here to use the readInput function
// to capture the response from the CIPCLOSE instruction
// to do so would mean we lose the entire HTML page we've
// just downloaded, so we use a different function here
readCloseInput(4000, "Unlink", "ERROR", true);
}else{
Serial.print(F("Can't contact "));
Serial.println(DEST_IP);
emptyWifiResponse();
}
}
void getHTMLResponse(){
unsigned long stop = millis()+(HTML_TIMEOUT*1000);
emptyWifiResponse();
int rec_state = 0;
int resp_len = 0;
bool quit=false;
int char_received;
bool html_header_found;
emptyWifiResponse();
ptr_index=0;
do {
while(mySerial.available()>0){
char b=mySerial.read();
// try just concatenating the character b to a string in this
// function and you'll quickly see why we're using a fixed
// length character array to receive the HTML from the server!
switch(rec_state){
case 0:
// get the first part of the response up to +IPD
wifi_response[ptr_index] = b;
ptr_index++;
if(wifiResponseContains("+IPD,")==true){
// this is the start of the server HTML response
if(in_debug==true){ Serial.println(F("+IPD, string found")); }
emptyWifiResponse();
rec_state=1;
}
break;
case 1:
// get the number of characters included in the reponse
// (so we know when to stop reading)
wifi_response[ptr_index] = b;
ptr_index++;
if(wifiResponseContains(":")==true){
// remove the colon from the end of the string
// and parse it into a value
ptr_index--;
wifi_response[ptr_index]=0;
// turn the character array into a string
// then use toInt to get the value
String t = "";
for(int it=0; it<ptr_index; it++){
t.concat(wifi_response[it]);
}
if(in_debug==true){
Serial.print(F("HTML character count: "));
Serial.println(t);
}
resp_len=t.toInt();
if(in_debug==true){
Serial.print(F("HTML response length: "));
Serial.println(resp_len,DEC);
}
emptyWifiResponse();
ptr_index=0;
char_received=0;
html_header_found=false;
rec_state=2;
}
break;
case 2:
// just keep receiving characters, adding them to the receive
// buffer, up to the end of the response
if(quit==false){
wifi_response[ptr_index] = b;
ptr_index++;
char_received++;
// if the received string is too long, the memory overrun can cause
// all kinds of weird stuff to happen, so keep it nice and short
// an HTML1.1 reponse separates the header of the message from the body
// using a double-CRLF entry
if(html_header_found==false){
if(wifiResponseContainsHeader() == true){
if(in_debug==true){ Serial.println(F("End of HTML header found")); }
html_header_found=true;
emptyWifiResponse();
}
}
// once you've received X characters, jump out of the
// timeout delay loop (ignore the last two characters
// as they're a CrLf combination)
if(char_received>=(resp_len-HTML_OVERHEAD) || b=='|'){
if(in_debug==true){
Serial.println(F("Full message received. Quitting read routine"));
}
quit=true;
}
}
break;
}
}
} while (millis() < stop && quit==false );
// null terminate the char array so we can Serial.print it
wifi_response[ptr_index]=0;
if(in_debug==true){
Serial.print(F("characters received: "));
Serial.println(char_received);
Serial.print(F("HTML response: "));
Serial.println(wifi_response);
}
}
void setup() {
Serial.begin(19200);
mySerial.begin(9600);
// set up the pins
pinMode(wifi_reset, OUTPUT);
digitalWrite(wifi_reset, LOW);
pinMode(button1_pin, INPUT_PULLUP);
Serial.println(F("Let's go"));
state=0;
}
void loop() {
// if we're not connected to a router, re-try periodically
if(has_ip==false){
flashLED(1);
connectToRouter();
delay(3000);
}else{
switch(state){
case 0:
// wait for the user to press a button to call the web page
b = digitalRead(button1_pin);
if(b==LOW){ state = 1; }
break;
case 1:
// after a successful call to this function, our character
// array buffer contains the contents of the HTML page so
// we can do whatever we like with it
getWebPageContents(F("/timekeeper/test.php"));
// do stuff with the wifi_response[i] buffer here to parse
// the response(s) from the web server
Serial.println(F("-----------------------"));
Serial.print(F("wifi_buffer contents: "));
Serial.println(wifi_response);
Serial.println("");
state=0;
}
}
}
Now we're fully aware that this code still has plenty of String uses throughout it. And Strings are bad. But our first attempt used nothing but Strings and - up to the point of retrieving an actual HTML page - worked really well. So we're in the process of updating the code to turn ALL instances of Strings into fixed length character arrays. But since the AT commands and responses are relatively small, and our incoming HTML buffer is a fixed 600 characters or so, there's plenty of available RAM to handle all the dynamic memory swapping that Arduino does to handle strings. So this is where we're up to at the minute.
Enjoy.
The basics are pretty simple - chuck a few strings into a few functions, send AT commands, catch the responses as strings and use the usual built-in parsing functions.
But when you start downloading entire web page contents (or, tbh, anything more than a couple of hundred bytes at a time) the responses get a little... well, confused it putting it mildly.
Now no-end of people will say "avoid Strings on an embedded controller". I'm one of them. But this was as much an exercise in making shareable Arduino code as much as it was about making a working solution. So while the very first impulse, when things started to go wrong, was "sod this, let's shove it on a PIC and use byte arrays", we had to resist - and shove it on an Arduino and use byte arrays....
There's a bit of confusion between Strings and byte/char arrays in Arduino-land. We haven't the scope to cover C++ programming concepts, pointers and memory locations here. But there's probably just about enough space to throw down some (working) code from which you can build your own embedded, web-based projects.
So let's get down to it....
The thing to remember is that we're working on an embedded system. An mcu with a massive 2k of memory. That's not a lot. So even though we're sending data to and from a web site, we need to write our web pages so as to keep the total character count down to a minimum.
We've built our input buffer to be about 600 characters. We might get away with a few hundred more. 1024 characters at push - the ESP8266 "chunks" large responses into 1024 bytes anyway. But that doesn't leave a massive amount of RAM left for all the other stuff we need to do. And it only takes one string of data to exceed the expected length, and we get all kinds of weird bugs and faults and corrupted data.
So although this code can retrieve data from a web page, it does so on the understanding that the web page itself is very very lightweight.
#include <LiquidCrystal.h>
#include <EEPROM.h>
#define wifi_reset 10
#define led_pin 13
#define button1_pin 12
#include <SoftwareSerial.h>
SoftwareSerial mySerial(8, 9); // RX, TX
// ------------ change this stuff --------------
String ssid = "YOUR_SSID";
String pwd = "YOUR_PWD";
String user = "Chris";
#define DEST_IP "188.226.228.99"
#define HTTP_REQ " HTTP/1.1\r\nHost: www.yourdomain.co.uk:80\r\n\r\n"
#define HTML_TIMEOUT 10
#define HTML_OVERHEAD 2
bool in_debug = true;
bool show_wifi_output = false;
// ---------------------------------------------
int state=0;
bool has_ip = false;
char b;
String cmd;
bool wifi_matched = false;
char wifi_response[500];
char *ptr;
int ptr_index;
String this_ip;
void emptyWifiResponse(){
memset(wifi_response, 0, sizeof(wifi_response));
ptr_index=0;
}
void writeAT(String at){
emptyWifiResponse();
if(in_debug==true){
Serial.print(F("Sending: "));
Serial.println(at);
}
// empty the incoming serial buffer
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
mySerial.println(at);
}
bool wifiResponseContainsHeader(){
bool b=false;
int k=0;
if(wifi_response[ptr_index-14]==':'){ k++;}
if(wifi_response[ptr_index-13]==' '){ k++;}
if(wifi_response[ptr_index-12]=='t'){ k++;}
if(wifi_response[ptr_index-11]=='e'){ k++;}
if(wifi_response[ptr_index-10]=='x'){ k++;}
if(wifi_response[ptr_index- 9]=='t'){ k++;}
if(wifi_response[ptr_index- 8]=='/'){ k++;}
if(wifi_response[ptr_index- 7]=='h'){ k++;}
if(wifi_response[ptr_index- 6]=='t'){ k++;}
if(wifi_response[ptr_index- 5]=='m'){ k++;}
if(wifi_response[ptr_index- 4]=='l'){ k++;}
// characters -3, -2, -1, 0 are the
// combination of CrLf x 2
if(k>=9){ b=true; }
return(b);
}
bool wifiResponseContains(String findThis){
// search the entire character array for the string
char __findThis[sizeof(findThis)];
findThis.toCharArray(__findThis, sizeof(__findThis));
ptr=strstr(wifi_response, __findThis);
if(ptr!=NULL){
return(true);
}else{
return(false);
}
}
void wifiResponse_Remove(String findThis){
// find a particular string in the char array and remove it
// (reducing the size of the array as necessary)
// we could use clever C functions like memmove etc. but for some
// functions behaviour is undefined if strings overlap, and for some
// they create a shadow copy before moving; we might not have the
// memory available to do this, so lets use the array itself
// get the index of the first character in the string
// we're trying to remove
char c;
int c_index=0;
int r_index=-1;
int match_count = 0;
int find_count = findThis.length();
int buff_len = 500-find_count;
char __findThis[find_count+1];
findThis.toCharArray(__findThis, find_count+1);
// walk through the response array, looking for the search string
for(int i=0; i<500; i++){
if(wifi_response[i]==__findThis[c_index]){
match_count++;
c_index++;
if(match_count==find_count){
// we've just found our string
r_index=(i-find_count)+1;
// quit the for-next loop early
i=500;
}
}else{
match_count=0;
c_index=0;
}
}
if(r_index > -1){
// remove x characters from the array by simply budging everything up
for(int i=r_index; i<buff_len; i++){
wifi_response[i] = wifi_response[i+find_count];
}
for(int i=buff_len+1; i<500; i++){ wifi_response[i]=0; }
}
}
bool checkIP(){
// first, send a CIFSR instruction then wait for a response
this_ip="0.0.0.0";
writeAT(F("AT+CIFSR"));
readInput(4000, "192.168.", "0.0.0.0", true);
if(wifi_matched==true){
if(wifiResponseContains("OK")==true){
wifiResponse_Remove("OK");
wifiResponse_Remove("AT+CIFSR");
// convert the char array into a string
// this_ip=wifi_response;
this_ip=wifi_response;
return(true);
}else{
return(false);
}
}else{
return(false);
}
}
void readInput(int timeout, String match, String err, bool exitOnFind) {
unsigned long stop = millis()+timeout;
emptyWifiResponse();
wifi_matched=false;
if(in_debug==true){
Serial.print(F("Looking for: "));
Serial.println(match);
}
if(show_wifi_output==true){
Serial.print(F("raw wifi >"));
}else{
Serial.print(F("wifi response: "));
}
// point to the start of the character array
ptr_index=0;
do {
while(mySerial.available()>0){
char b=mySerial.read();
if(show_wifi_output==true){ Serial.print(b);}
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
if(wifiResponseContains(err)==true){
// error condition has been matched
if(in_debug==true){ Serial.println(F("Error condition matched")); }
stop=millis()-1;
// read the rest of the buffer just to empty it
emptyWifiResponse();
while(mySerial.available()>0){
b = mySerial.read();
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
}
break;
}
if(wifiResponseContains(match) == true ){
// requested string has been matched
// so now return everything after the matched word
if(in_debug==true){
Serial.println(F("Match condition found"));
Serial.println();
}
wifi_matched = true;
if(exitOnFind==true){
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){
b = mySerial.read();
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
wifi_response[ptr_index] = b;
ptr_index++;
}
}
break;
}
}
}
} while (millis() < stop);
// this is a bit hacky, but it'll have to do.
// sometimes, when we're expecting OK we get "no change"
// but if there's been no change, that's the same as
// OK, so let's deal with that
if(show_wifi_output==true){
Serial.println(F("<"));
Serial.print(F("Buffered response: "));
}
if(in_debug==true){ Serial.println(wifi_response); }
if(match=="OK" && wifiResponseContains("no change")==true){
if(in_debug==true){ Serial.print(F(" OK/no change (close enough) ")); }
wifi_matched=true;
}
if(in_debug==true){
Serial.print(F("match found: "));
Serial.println(wifi_matched);
}
}
void readCloseInput(int timeout, String match, String err, bool exitOnFind) {
unsigned long stop = millis()+timeout;
cmd="";
bool closeOK = false;
// because the response from an AT command is small, we're using this
// simplified function (complete with Strings!) to check the response
// to the CIPCLOSE command, without wiping out the character array
// buffer (which at this point contains the HTML we want to parse)
do {
while(mySerial.available()>0){
char b=mySerial.read();
if(show_wifi_output==true){ Serial.print(b);}
if(b==0x0a || b==0x0d || b < 0x20 || b > 0x80){
// ignore these characters
}else{
cmd.concat(b);
}
if(cmd.indexOf(err)>=0){
// error condition has been matched
if(in_debug==true){ Serial.println(F("Error condition matched")); }
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){ b = mySerial.read(); }
break;
}
if(cmd.indexOf(match)>=0 ){
// requested string has been matched
// so now return everything after the matched word
if(in_debug==true){ Serial.println(F("Match condition found")); }
closeOK = true;
if(exitOnFind==true){
stop=millis()-1;
// read the rest of the buffer just to empty it
while(mySerial.available()>0){ b = mySerial.read(); }
break;
}
}
}
} while (millis() < stop);
if(in_debug==true){
Serial.print(F("Closed ok: "));
Serial.println(closeOK);
}
}
void flashLED(int k){
for(int i=0; i<k; i++){
digitalWrite(led_pin,HIGH);
delay(250);
digitalWrite(led_pin,LOW);
delay(500);
}
}
void connectToRouter(){
String p;
// hardware reset the module
if(in_debug==true){ Serial.println(F("Resetting wifi"));}
digitalWrite(wifi_reset, LOW);
delay(500);
digitalWrite(wifi_reset, HIGH);
// Look for ready string from wifi module
readInput(4000, "Ready", "Error", true);
if(in_debug==true){
Serial.print(F("Wifi read: "));
Serial.println(wifi_response);
}
if(wifi_matched==true){
writeAT(F("AT+CWMODE=1"));
readInput(2500, "OK", "Error", true);
if(in_debug==true){
Serial.print(F("Wifi: "));
Serial.println(wifi_response);
}
if(wifi_matched==true || wifi_matched!=true){
delay(1000);
has_ip=checkIP();
if(has_ip==false){
p = "AT+CWJAP=\"";
p.concat(ssid);
p.concat("\",\"");
p.concat(pwd);
p.concat("\"");
writeAT(p);
// there's not really an OK message with CWJAP
// just returns a string when completed
readInput(16000, "OK", "FAIL", true);
if(wifiResponseContains("FAIL")==true){
// can't connect to the access point
Serial.println(F("Can't get onto the access point"));
}else if(wifi_matched==true){
// so now query the system for an IP address again
has_ip=checkIP();
if(has_ip==true){
Serial.print(F("This ip address is: "));
Serial.println(this_ip);
}else{
// access point refused
Serial.println(F("Refused access to AP"));
}
}else{
Serial.println(F("No response from AP"));
}
}
}
}else{
Serial.println(F("Unable to reboot wifi module"));
}
}
void getWebPageContents(String url){
cmd="AT+CIPSTART=\"TCP\",\"";
cmd.concat(DEST_IP);
cmd.concat("\",80");
writeAT(cmd);
readInput(8000, "Linked", "ERROR", true);
if(wifi_matched==true){
// tell the server how many bytes to expect
// by first building the request then getting
// a character count (including trailing \r\n)
delay(100);
cmd = "GET ";
cmd += url;
cmd += "?user=";
cmd += user;
cmd += HTTP_REQ;
String c = "AT+CIPSEND=";
c.concat(String(cmd.length()));
writeAT(c);
readInput(4000, ">", "Error", true);
if(wifi_matched==true){
// now send the request to actually get some data
delay(500);
emptyWifiResponse();
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
if(in_debug==true){
Serial.print(F("Sending: "));
Serial.println(cmd);
}
mySerial.print(cmd);
// now we can't just use our readInput as the response could be in multipart chunks
// so we need to parse this separately
getHTMLResponse();
}else{
Serial.println(F("Can't enter send data mode"));
emptyWifiResponse();
}
// now close the connection
delay(300);
// empty the incoming serial buffer
while(mySerial.available()>0){ mySerial.read(); }
// send the AT command to the wifi module
mySerial.println(F("AT+CIPCLOSE"));
// while it's tempting here to use the readInput function
// to capture the response from the CIPCLOSE instruction
// to do so would mean we lose the entire HTML page we've
// just downloaded, so we use a different function here
readCloseInput(4000, "Unlink", "ERROR", true);
}else{
Serial.print(F("Can't contact "));
Serial.println(DEST_IP);
emptyWifiResponse();
}
}
void getHTMLResponse(){
unsigned long stop = millis()+(HTML_TIMEOUT*1000);
emptyWifiResponse();
int rec_state = 0;
int resp_len = 0;
bool quit=false;
int char_received;
bool html_header_found;
emptyWifiResponse();
ptr_index=0;
do {
while(mySerial.available()>0){
char b=mySerial.read();
// try just concatenating the character b to a string in this
// function and you'll quickly see why we're using a fixed
// length character array to receive the HTML from the server!
switch(rec_state){
case 0:
// get the first part of the response up to +IPD
wifi_response[ptr_index] = b;
ptr_index++;
if(wifiResponseContains("+IPD,")==true){
// this is the start of the server HTML response
if(in_debug==true){ Serial.println(F("+IPD, string found")); }
emptyWifiResponse();
rec_state=1;
}
break;
case 1:
// get the number of characters included in the reponse
// (so we know when to stop reading)
wifi_response[ptr_index] = b;
ptr_index++;
if(wifiResponseContains(":")==true){
// remove the colon from the end of the string
// and parse it into a value
ptr_index--;
wifi_response[ptr_index]=0;
// turn the character array into a string
// then use toInt to get the value
String t = "";
for(int it=0; it<ptr_index; it++){
t.concat(wifi_response[it]);
}
if(in_debug==true){
Serial.print(F("HTML character count: "));
Serial.println(t);
}
resp_len=t.toInt();
if(in_debug==true){
Serial.print(F("HTML response length: "));
Serial.println(resp_len,DEC);
}
emptyWifiResponse();
ptr_index=0;
char_received=0;
html_header_found=false;
rec_state=2;
}
break;
case 2:
// just keep receiving characters, adding them to the receive
// buffer, up to the end of the response
if(quit==false){
wifi_response[ptr_index] = b;
ptr_index++;
char_received++;
// if the received string is too long, the memory overrun can cause
// all kinds of weird stuff to happen, so keep it nice and short
// an HTML1.1 reponse separates the header of the message from the body
// using a double-CRLF entry
if(html_header_found==false){
if(wifiResponseContainsHeader() == true){
if(in_debug==true){ Serial.println(F("End of HTML header found")); }
html_header_found=true;
emptyWifiResponse();
}
}
// once you've received X characters, jump out of the
// timeout delay loop (ignore the last two characters
// as they're a CrLf combination)
if(char_received>=(resp_len-HTML_OVERHEAD) || b=='|'){
if(in_debug==true){
Serial.println(F("Full message received. Quitting read routine"));
}
quit=true;
}
}
break;
}
}
} while (millis() < stop && quit==false );
// null terminate the char array so we can Serial.print it
wifi_response[ptr_index]=0;
if(in_debug==true){
Serial.print(F("characters received: "));
Serial.println(char_received);
Serial.print(F("HTML response: "));
Serial.println(wifi_response);
}
}
void setup() {
Serial.begin(19200);
mySerial.begin(9600);
// set up the pins
pinMode(wifi_reset, OUTPUT);
digitalWrite(wifi_reset, LOW);
pinMode(button1_pin, INPUT_PULLUP);
Serial.println(F("Let's go"));
state=0;
}
void loop() {
// if we're not connected to a router, re-try periodically
if(has_ip==false){
flashLED(1);
connectToRouter();
delay(3000);
}else{
switch(state){
case 0:
// wait for the user to press a button to call the web page
b = digitalRead(button1_pin);
if(b==LOW){ state = 1; }
break;
case 1:
// after a successful call to this function, our character
// array buffer contains the contents of the HTML page so
// we can do whatever we like with it
getWebPageContents(F("/timekeeper/test.php"));
// do stuff with the wifi_response[i] buffer here to parse
// the response(s) from the web server
Serial.println(F("-----------------------"));
Serial.print(F("wifi_buffer contents: "));
Serial.println(wifi_response);
Serial.println("");
state=0;
}
}
}
Now we're fully aware that this code still has plenty of String uses throughout it. And Strings are bad. But our first attempt used nothing but Strings and - up to the point of retrieving an actual HTML page - worked really well. So we're in the process of updating the code to turn ALL instances of Strings into fixed length character arrays. But since the AT commands and responses are relatively small, and our incoming HTML buffer is a fixed 600 characters or so, there's plenty of available RAM to handle all the dynamic memory swapping that Arduino does to handle strings. So this is where we're up to at the minute.
Enjoy.
Wednesday, 28 September 2016
ESP8266 upgrade woes
While hacking strings and character arrays around is pretty normal in the microcontroller world, when you're sending serial data as text, it's also a laborious pain to code up. Which is probably why most Arduino examples using the ESP8266 wifi module use the esp8266.h library.
Which is great.
Except it doesn't work with our firmware.
Once again, we're up against the idea of library abstraction.
While hiding everything away and making it accessible through a few functions makes your main code look neat, that only holds true for as long as the library works! And when you've built a project based on a library that doesn't work, it's often more hassle trying to untangle someone else's code than it would have been to build your own from the ground up.
Anyway, we struggled with the esp8266.h library for a couple of reasons: First up, our wifi SSID is 18 characters long. The esp8266.h library has buffers for 16 character strings. It's a quick fix, once you know where to look - but infuriating for a long while, when the command
AT+CWJAP = "SSIDquitealongname","password"
was being sent to the wifi module as
AT+CWJAP = "SSIDquitealongnamepassword","password".
No wonder we couldn't connect to the router! (the reason, presumably, being that our SSID character array did not contain a \0 trailing character to indicate the end of a string, so the code/library/mcu ran on into the next memory space, until it found the \0 byte at the end of our password. However, there's no guarantee that the SSID and password strings would be in continuous sections of memory space - it could just have easily been a load of random garbage!
To fix this, we had to change the esp8266.h file and increase the buffer size
And then in the esp8266.cpp file, because we didn't bother working out which functions were used to handle the SSID/password combination, wherever there was a character array of 16 characters, we increased the buffer size to 20.
This at least allowed us to connect to the router successfully and to obtain an IP address.
But even then, we had to do a bit more hackery. See, the ESP8266 library expects certain responses from the wifi module. One of these responses is that when the AT+CIFSR instruction is sent, the response includes the character pattern STAIP followed by the IP address
Our (updated) wifi modules don't do this. They simply split out the IP address followed by OK.
So instead of looking for a response of STAIP, we had to know the beginning range of our IP address, and look for that instead.
In fact, we found the esp8266 library full of little things like this - expected responses and strings either chopped up or entirely discarded, depending on what the firmware expected to receive (rather than what the actual response was).
In short, it looks like we're going to have to abandon the esp8266 library and write something ourselves. And if you've updated your firmware using the same files as we did a little while back (so your firmware boots up looking like the screenshot below) you might have to do the same!
Which is great.
Except it doesn't work with our firmware.
Once again, we're up against the idea of library abstraction.
While hiding everything away and making it accessible through a few functions makes your main code look neat, that only holds true for as long as the library works! And when you've built a project based on a library that doesn't work, it's often more hassle trying to untangle someone else's code than it would have been to build your own from the ground up.
Anyway, we struggled with the esp8266.h library for a couple of reasons: First up, our wifi SSID is 18 characters long. The esp8266.h library has buffers for 16 character strings. It's a quick fix, once you know where to look - but infuriating for a long while, when the command
AT+CWJAP = "SSIDquitealongname","password"
was being sent to the wifi module as
AT+CWJAP = "SSIDquitealongnamepassword","password".
No wonder we couldn't connect to the router! (the reason, presumably, being that our SSID character array did not contain a \0 trailing character to indicate the end of a string, so the code/library/mcu ran on into the next memory space, until it found the \0 byte at the end of our password. However, there's no guarantee that the SSID and password strings would be in continuous sections of memory space - it could just have easily been a load of random garbage!
To fix this, we had to change the esp8266.h file and increase the buffer size
And then in the esp8266.cpp file, because we didn't bother working out which functions were used to handle the SSID/password combination, wherever there was a character array of 16 characters, we increased the buffer size to 20.
This at least allowed us to connect to the router successfully and to obtain an IP address.
But even then, we had to do a bit more hackery. See, the ESP8266 library expects certain responses from the wifi module. One of these responses is that when the AT+CIFSR instruction is sent, the response includes the character pattern STAIP followed by the IP address
Our (updated) wifi modules don't do this. They simply split out the IP address followed by OK.
So instead of looking for a response of STAIP, we had to know the beginning range of our IP address, and look for that instead.
In fact, we found the esp8266 library full of little things like this - expected responses and strings either chopped up or entirely discarded, depending on what the firmware expected to receive (rather than what the actual response was).
In short, it looks like we're going to have to abandon the esp8266 library and write something ourselves. And if you've updated your firmware using the same files as we did a little while back (so your firmware boots up looking like the screenshot below) you might have to do the same!
Subscribe to:
Posts (Atom)


















