Hey Everybody!
So, I wanted to share something I'm working on...
https://drive.google.com/file/d/1HNzWdlbBMoqUzqix8OtQSUFrHMX1J9o_/view https://drive.google.com/file/d/1Iff2KFSkHupMyhlnKSCuWGRt8POf0xT4/view As I'm sure most people here are familiar with, developing and debugging large projects directly on the Arduino/ESP32 is time consuming and frustrating. Most of the time, the buggy code we're trying to make work is just standard C/C++ code which is not particular to the Arduino hardware itself, and thus doesn't really need to run on-board to debug.
For awhile in the past, I was writing parts of my code in a C++ console app, debugging it and then copy/pasting it into my Arduino project, but that comes with all kinds of unique problems and limitations, especially when you want to go back to developing it, after it's already been integrated into the Arduino project. It quickly becomes the kind of mess you just don't want to look at. LOL...So, I was looking for different solutions to this for awhile, and finally discovered a post in this forum about Unit Testing, which I'd heard about, but hadn't looked into.
Yeah, when I discovered the Shared Code Unit Testing about 2 months ago, I was absolutely floored! It seemed to be the exact solution I was looking for, and I was able to make some good progress on my project using it, but then I really hit the limits of it. I'm really not a fan of the whole Assert thing, and the lack of a console window for output is a bit frustrating. I get that it has its place, and I'm realizing it's really meant for testing code for production, but not so much for developing code in early stages.
However, thanks to learning how to do the shared code and unit testing thing, in Visual Micro, I had a bit of a Eureka and decided to see if I could add a C++ console app to my shared code project, the same way a Unit Test is added, and much to my absolute delight, it worked!
Although, one of the most obvious problems that occurs, with both Unit Testing and with running an Arduino Library in the console, is the actual Arduino code. Like, include "arduino.h" in any header, and you're in a for a world of hurt. Commenting out and then re-implementing arduino lines, like a Serial.print() or pinMode(), is just an inefficient non-viable option, but it's what I tried at first. So, instead I came up with this:
#include "AutoBoardFinder.h"
#if(__WINDUINO_BUILD_)
#include "WINduino.h"
#else
#include "arduino.h"
#endif
First, AutoBoardFinder.h uses preprocess logic to figure out what board the project is being compiled for, which I had already started developing to create cross-compatibility of my library project for all the boards I'm using, but I added logic to detect if the project was being built in the Window's Environment as well. Once I had that, I could then use it to implement the WINduino.h library instead of the arduino.h library. (NOTE: I'm hoping to expand this to Linux at some point as well).
Now, the WINduino library is actually a pretty simple idea. Here's a bit of what it looks like:
#if(__WINDUINO_BUILD_)
#define OUTPUT 0x03
#define INPUT 0x01
#define INPUT_PULLUP 0x05
#define INPUT_PULLDOWN 0x09
#define LOW 0x0
#define HIGH 0x1
#define CHANGE 0X03
#define RISING 0x01
#define FALLING 0x02
#define digitalPinToGPIONumber(p) p
#include <iostream>
#include <chrono>
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef signed char int8_t;
extern unsigned long __DummyBoardStartTime;
unsigned long millis();
unsigned long micros();
namespace pincontrol {
namespace _ {
static const uint8_t pinRegisterCapacity = 50;
extern int digitalPinValue[pinRegisterCapacity];
static const uint8_t winduinoIndentOptions = 6;
extern const char* winduinoIndent[winduinoIndentOptions];
extern uint8_t winduinoIndentChosenOption;
}
/* Returns TRUE if pin number is out of range. value for digital pins is set to 0 for
anything less than 1, and set to 1 for 1 or anything greater than 1.*/
bool SimulateDigitalPinValue(uint8_t pinNo, uint8_t value);
bool SetWinduinoMessageIndent(const uint8_t& indentOption);
}
void pinMode(uint8_t pinNumber, uint8_t MODE);
void attachInterrupt(uint8_t, void (*)(), int mode);
void delay(uint32_t millisecondDelay);
uint16_t analogRead(uint8_t pinNumber);
int digitalRead(uint8_t pinNumber);
void digitalWrite(uint8_t pinNumber, uint8_t value);
class SerialDummy {
int i = 0;
public:
SerialDummy() : i(1) {}
uint8_t print(const char* in) { std::cout << in; return 1; }
uint8_t print(unsigned long in, int = 10) { std::cout << in; return 1; }
void begin(unsigned long baud);
};
extern SerialDummy serialDummy;
extern SerialDummy Serial;
namespace _ {
const char* InputOutputDecode(uint8_t mode);
const char* DecodeInterruptMODE(uint8_t mode);
unsigned long GetNowTime();
}
class Print {
public:
size_t print(const char[]) {
return 0;
}
private:
int write_error;
};
class Printable {
public:
virtual ~Printable() {}
virtual size_t printTo(Print& p) const = 0;
};
#endif // __WINDUINO_BUILD_
At first, I just wanted to create "placeholders" for the Arduino code, mainly just to prevent compile errors, but then quickly realized I could wrap cout output with the serial class, and not only leave my Serial.print()s intact, but keep them useful and outputting where I needed them, such as with non-debugging user interface outputs. Further I realized I could make the delay() and millis() functional too, but then I started to work on pin logic, and got to thinking of ways I could make them more useful too.
Here's a bit of what's happening in the WINduino.cpp file:
void pinMode(uint8_t pinNumber, uint8_t MODE)
{
if (pinNumber >= pincontrol::_::pinRegisterCapacity) {
std::cout << "\n\n\n\t*** ERROR ***\n\tpinMode() called with Out of Range Pin Number : " << uint32_t(pinNumber) << "\n\n\tPIN NOT SET UP FOR SIMULATION!";
return;
}
switch (MODE)
{
case INPUT:
pincontrol::_::digitalPinValue[pinNumber] = 0;
break;
case INPUT_PULLUP:
pincontrol::_::digitalPinValue[pinNumber] = 1;
break;
case INPUT_PULLDOWN:
pincontrol::_::digitalPinValue[pinNumber] = 0;
break;
default:
break;
}
std::cout << pincontrol::_::winduinoIndent[pincontrol::_::winduinoIndentChosenOption] << "WINduino :: PIN # " << uint32_t(pinNumber) << " Set as : " << _::InputOutputDecode(MODE);
}
unsigned long millis() {
return _::GetNowTime() - __DummyBoardStartTime;
}
void delay(uint32_t millisecondDelay)
{
unsigned long nowTime = _::GetNowTime();
unsigned long endTime = nowTime + millisecondDelay;
while (true)
{
if (nowTime >= endTime) return;
nowTime = _::GetNowTime();
}
}
int digitalRead(uint8_t pinNumber)
{
if (pinNumber >= pincontrol::_::pinRegisterCapacity) {
std::cout << "\n\n\n\t*** ERROR ***\n\tdigitalRead() called with Out of Range Pin Number : " << uint32_t(pinNumber) << "\n\n\tUNABLE TO SIMULATE!";
return 0;
}
std::cout << pincontrol::_::winduinoIndent[pincontrol::_::winduinoIndentChosenOption] << "WINduino :: Digital Read : Pin Number = " << uint32_t(pinNumber) << "\n";
return pincontrol::_::digitalPinValue[pinNumber];
}
void SerialDummy::begin(unsigned long baud)
{
std::cout << pincontrol::_::winduinoIndent[pincontrol::_::winduinoIndentChosenOption] << "\n\nWINduino :: Serial Initialized to Baud Rate of " << baud;
}
That's where I'm at right now, and so far I'm just doing a very superficial "simulation" of the pins, by just creating a fake digital pin register I can write to and read from, with an array. It works pretty good, I have to say, as it has allowed me to develop my pin manager, completely off-board, so far. I mean, I don't actually need a pin to do most of the development, and just more or less need checkpoints and something to return a fake pin value, to make sure the logic works, which is pretty easy to do.
Multiple times already, my code threw an exception. You know, the usual stuff. Forgetting to initialize a pointer, or reading an out of range array index. If this had happened on-board, I'd probably still be chasing down the bugs, but having the full debugging power of Visual Studio taking me right to the point of the exception, and I was fixing these problems within mere minutes, and then instantly re-running my corrected code again. I just... The value in that, is so huge.
However, I do want to do more with this, and at some point, I would actually like to have more of an emulation/simulation of a board with a windows side application, where I can maybe attach simple components like buttons or LEDs, to do asynchronous manual inputs or visually see outputs. But, this is where I start to get out of my depth again. Like, I have no idea how I would simulate an interrupt, because I'm not advanced enough to figure out how I could change control flow, if for example, an interrupt happens during a while loop. Also, I'm not sure if it's possible, but it would be great to somehow throttle the processing speed to be more realistic to the target board the code is intended for too.
And finally, I've used some things like TinkerCAD to emulate Arduinos, and honestly, TinkerCAD is really useless for this, even on a basic project, plus it doesn't have many board options and none of the boards I'm using. But, even if it was a better simulator, it's not really what I want anyways. I mean, I don't want to just compile and hand my code off to this emulator, outside of the IDE, because that really feels like going right back to the initial problem of developing and debugging on-board. Best I can describe it is that I still want to develop and debug inside Visual Studio with Visual Micro, and fully utilize all of it's utilities with a running console application and debug or Unit Testing sessions. The external app would only really emulate a user interface for the pins, and not even try to replicate the MCU processor itself. Just somehow interface in the background, maybe using shared memory or something, to manipulate fake pin registers. Either way, I want the work to stay primarily in the IDE.
Anyways, I wanted to share this to get thoughts on it. Maybe it's all a really dumb idea and I just don't see it, or there's better ways to achieve what I want already out there. Or maybe there isn't and others with more experience than me kind of want the same thing for their own development workflow, and who could share ideas, methods or information to help me get there. Even critiques of how I'm handling things, based on what I've posted, are welcome. I'm a self-taught DIYer and still have a lot to learn.
PS: I do intend to put this whole project on GitHub once it's more mature, but it's still quite a mess at the moment, and I don't feel completely comfortable putting it all out there yet.
PPS: In case anyone is wondering, the trick to running the c++ project is to set it as your Startup project, rather than your Arduino project. And you can just switch this at any time without problem, when you want to compile the Arduino project. Only thing that sucks a bit, is the vMicro menu will load-in and load-out when you're switching between different files, with the c++ set as the startup. At some point, I'd like to explore if there's a way to change this behaviour, but learning all about toolchains and build systems is something I still have to do, so I'm just not touching it yet.