C-like interpreter that actually fits and runs inside most Arduino chips: wrench

The ESP32 can update a sketch via Wifi with OTA, so you have to come up with something better, for example running a webserver on a ESP32 that has a page with a text field that accepts wrench source code.
If you have more examples and it also runs on a Arduino Uno, then the website hackaday might be interested.

Can you add HTTPS to your website ? Your provider has probably a free option to select it. Why the "wrench/www/", can you do just "https://northarc.com" with a button to go to "http://northarc.com/wrench/".
When a website has no HTTPS, that usually means that it has been left unattended for more than 10 years.

The idea of running an interpreter is interesting for the case where your end-user is adjusting the code, if you have a tool-stack and are comfortable modifying the firmware then that will be always be preferable I would think. I love the idea of an embedded webpage that accepts code though and can run it against an API, as long as there was watchdog protection.

You are certainly right that examples are what will drive adoption. I'll plan that for my next pass.

I run my own webserver and haven't bothered to get a cert, I'll add it to my list.

No, not if the version didn't change :stuck_out_tongue_winking_eye:

The next step would be controlling Neopixels and servo motors and displays via functions that have the same look and feel as the real libraries. The callback function would just call the real library function.
How can I do this:

// Some glue for Arduino things
enum { INPUT = 0, OUTPUT = 1};
enum { LOW = 0, HIGH = 1};

// Interface to the Servo library
struct Servo
{
  attach(pin);
  write(angle);
};
Servo servo1;
Servo servo2;

// The wrench code as Arduino-alike code
potPin = 32;
servo1.attach(2);

while( true)           // wrench has no Arduino loop()
{
  x = analogRead( potPin);
  angle = x / 25;
  servo1.write(angle);

  print( x);
  print( "\n");
  delay( 250);
}

[ADDED]
This is from your example:

const char* wrenchCode = 
"print( \"Hello World!\\n\" );"

This is how it could be one day, if the Arduino preprocessor is updated to a better ctags:

const char* wrenchCode = R"=====(
print("Hello World!\n");
)=====";

okay I see where you're going with this, I'll take a pass at it maybe tonight or tomorrow.

Sounds like Javascript to me ...

Espruino - JavaScript for Microcontrollers

Please be aware that shortly after adoption, your "baby" will be driven by the wants/needs of others through feature requests; it is easy to lose control of the original product through scope-creep. I'm certain as a professional developer you are aware that "small/efficient" is often lost when features are added beyond the original intent. Then there is that reality of never having enough "freetime" to allocate to code diverging from the original purpose.

Thanks for sharing this cool project @tinker_curt!

No. The Arduino build system automatically adds the #include directive for Arduino.h to the C++ file generated from the .ino files of the sketch during sketch preprocessing, but it only does that if the code in the .ino files does not already contain an #include directive for that file:

https://arduino.github.io/arduino-cli/dev/sketch-build-process/#pre-processing:~:text=If%20not%20already%20present

So you get the #include directive one way or another.

I recommend against adding this #include directive to sketches targeted to beginners because it adds unnecessary extra complexity to something the average beginner will already find very complex. But I don't think your project is targeted to beginners so they do no harm in your sketch, and might actually make the code easier to understand for any users who are experienced in C++ in general, but not in the few unique aspects of Arduino sketches such as the automatic addition of this #include directive.

I agree with mrburnette. Keep it small. I'm just exploring the possibilities.

Meanwhile, it is now also running on a Uno.

What sets the output length of a floating point number ?
I only get "3.14159".
The project to compile and run wrench code to calculate Pi on a Raspberry Pi Pico in Wokwi is here: https://wokwi.com/projects/347808271933899348

Yeah if you have the system for it. The chips that support the kind of horsepower js requires are ARM-based with gobs of memory and flash. If that's your platform then by all means go big, run whatever.

wrench is for when you don't have that. the Uno Micro has 2k of RAM and 32k of flash yet wrench operates there fully functional and full featured. Maybe that kind of frugality will be useful to some use cases, and probably not, but comparing it to js is apples/oranges.

What is the verdict ? Just use servo1_attach() and servo1_write() in wrench ?
Now that I used math::sqrt(), can I make my own servo::attach() ?

It's cosmetic, the wr_ftoa() is a very simplified implementation for the purposes of asString() with a hard-coded 5 digits of precision. I did not waste code space formatting past the very basics, if the developer wants better than usually the built in sprintf() should be engaged. Although I did provide a very lean-and-mean version in std_string str::sprintf() and str::printf() for use inside wrench code.

Yes this is precisely what I was going to suggest. I'll take some time today and fully document the library-writing process, it's not complicated but is designed for maximum speed so its a little twisted to match the wrench calling convention.

Really impressed with the work you're putting into this, I'll be sure to add you to the contributions page :slight_smile:

Okay I added an "extending wrench" section to the webpage that details how to write library callbacks.

The example with the Raspberry Pi Pico is extended, and wrench didn't let me down :smiley:
Any improvements that I can make ?

I will show the code here, to show to others what it looks like:

// FloatingWrench.ino
//
// 9 nov 2022
//
// Test sketch for the floating point calculations of the 'wrench' interpreter.
// When the simulation is running, click on the NTC module and change the settings.
//
// This Wokwi project: https://wokwi.com/projects/347808271933899348
//
// wrench repository: https://github.com/jingoro2112/wrench
// wrench website: http://northarc.com/wrench/www/
// I learned about 'wrench' here: https://forum.arduino.cc/t/c-like-interpreter-that-actually-fits-and-runs-inside-most-arduino-chips-wrench/1050025/
//
// Old times: The processor executes code.
// Modern times:
//     The wrench code is compiled into binary code.
//     That binary code is executed by the wrench interpreter.
//     The wrench interpreter runs on top of Arduino code.
//     The Arduino code runs on top of Mbed.
//     The Mbed runs on low level Raspberry Pi Pico code.
//     The low level Raspberry Pi Pico code is compiled.
//     The compiled code runs in JavaScipt.
//     The JavaScript code runs in your browser.
//     Your browser uses your operating system and your processor.
//

#include <Wire.h>
#include <hd44780.h>                       // The hd44780 library
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header

#include "wrench.h"

hd44780_I2Cexp lcd;      // declare lcd object: auto locate & auto config expander chip
const int LCD_COLS = 16;
const int LCD_ROWS = 2;


// The wrench code that will be compiled runtime.
// Using the "raw string literal" or "Super Quotes"
// https://en.cppreference.com/w/cpp/language/string_literal
//
const char* wrenchCode = R"=====(
// -------------------------------------------------
// Start of wrench source code
// -------------------------------------------------

// -------------------- Glue code ------------------

enum { INPUT = 0, OUTPUT = 1};      // make own definitions
enum { LOW = 0, HIGH = 1};


// ----------------- Start of Arduino-alike code ----------------

print( "Hello 😀    ");                  // also testing UTF-8
println( "This is wrench code 🔧");


// ----------------- Calculating Pi ----------------

println( "Calculating π");

two = 2.0;
s = 0.0;
t = 1.0;
for( i = 0; i < 11; i++)
{
  r = s + two;
  s = math::sqrt(r);
  t *= s / two;

  pi = two / t;
  print( pi);
  print( "    ");
  if( (i + 1) % 4 == 0)              // four values per line
    println();
}
println();

// --------- Show temperature on LCD display ------------

ntcPin = 26;
ledPin = 22;
ledStatus = LOW;

pinMode( ledPin, OUTPUT);

status = lcd::begin( 16, 2);         // columns, rows 
if( status != 0)
{
  println( "Error, LCD was not found");
}

while(true)
{
  pos = 0;                    // keep track of how many characters are writen
  lcd::setCursor(0,0);
  pos += lcd::print( "T = ");

  // Formula taken from: https://wokwi.com/projects/299330254810382858 (C)Uri Shaked
  BETA = 3950.0;                   // should match the Beta Coefficient of the thermistor
  analogValue = analogRead( ntcPin);
  celsius = 1.0 / (math::log(1.0 / (1023.0 / analogValue - 1.0)) / BETA + 1.0 / 298.15) - 273.15;
  pos += lcd::print( celsius);

  // clear the rest of the line
  for( i=pos; i<16; i++)
    lcd::print( " ");

  // Blink the led
  if( ledStatus == LOW)
    ledStatus = HIGH;
  else
    ledStatus = LOW;

  digitalWrite( ledPin, ledStatus);

  delay( 300);
} 

// -------------------------------------------------
// End of wrench source code
// ------------------------------------------------- */
)=====";


// The Arduino preprocessor gets confused, therefor function prototyping is required
void print( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void println( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void delay( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void pinMode( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void digitalWrite( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void analogRead( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr);
void lcd_begin( WRValue* stackTop, const int argn, WRContext* c);
void lcd_setCursor( WRValue* stackTop, const int argn, WRContext* c);
void lcd_print( WRValue* stackTop, const int argn, WRContext* c);


void setup()
{
  Serial.begin( 115200 );
  Serial.println( "FloatingWrench.ino");
  Serial.print( "wrench version: ");
  Serial.print( WRENCH_VERSION_MAJOR);
  Serial.print( ".");
  Serial.println( WRENCH_VERSION_MINOR);
  Serial.println( "┃ To compile the wrench source code,     ┃");
  Serial.println( "┃ remove #define WRENCH_WITHOUT_COMPILER ┃");
  Serial.println( "┃ from wrench.h                          ┃");


  WRState* w = wr_newState(); // create the state

  // Bind the functions.
  // The Math functions are provide by wrench.
  // The normal functions can even have the same names.
  // Adding a library function uses basic wrench functionality.
  wr_loadMathLib( w);                   // register all the math functions
  wr_registerFunction( w, "print", print);
  wr_registerFunction( w, "println", println);
  wr_registerFunction( w, "delay", delay);
  wr_registerFunction( w, "pinMode", pinMode);
  wr_registerFunction( w, "digitalWrite", digitalWrite);
  wr_registerFunction( w, "analogRead", analogRead);
  wr_registerLibraryFunction( w, "lcd::begin", lcd_begin);
  wr_registerLibraryFunction( w, "lcd::setCursor", lcd_setCursor);
  wr_registerLibraryFunction( w, "lcd::print", lcd_print);

  unsigned char* outBytes; // compiled code is alloc'ed
  int outLen;
  int err = wr_compile( wrenchCode, strlen(wrenchCode), &outBytes, &outLen); // compile it

  Serial.print( "The Compiled code is ");
  Serial.print( outLen);
  Serial.println( " bytes");

  if( err == 0)
  {
    wr_run( w, outBytes); // load and run the code!
    delete[] outBytes;    // clean up
  }
  else
  {
    Serial.print( "Compiling error: ");
    Serial.println( err);
    Serial.print( "Did you forget to remove the #define WRENCH_WITHOUT_COMPILER");
  }

  wr_destroyState( w );
}

void loop()
{
  delay( 10);                                     // a delay in the loop is better for Wokwi
}

// The functions convert the wrench code to the specific platform code.

void print( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  char buf[128];
  for( int i=0; i<argn; ++i)
  {
    Serial.print( argv[i].asString(buf, sizeof(buf)));
  }
}

void println( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  char buf[128];
  for( int i=0; i<argn; ++i)
  {
    Serial.print( argv[i].asString(buf, sizeof(buf)));
  }
  Serial.println();            // allow no arguments for just a newline
}

void delay( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  if( argn == 1)
  {
    delay( (unsigned long)argv[0].asInt());       // a signed int will work up to 25 days
  }
}

// No translation is needed for the LOW and HIGH in the wrench code,
// they seem to be 0 and 1 no every platform.
// The INPUT and OUTPUT can have differenct numbers on different platforms,
// those need an extra translation.
void pinMode( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  if( argn == 2)
  {
    if ( argv[1].asInt() == 1)                  // wrench value for OUTPUT
    {
      pinMode( argv[0].asInt(), OUTPUT);        // Arduino value for OUTPUT
    }
    else if ( argv[1].asInt() == 0)             // wrench value for INPUT
    {
      pinMode( argv[0].asInt(), INPUT);         // Arduino value for INPUT
    }
  }
}

void digitalWrite( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  if( argn == 2)
  {
    digitalWrite(argv[0].asInt(), argv[1].asInt());
  }
}

void analogRead( WRState* w, const WRValue* argv, const int argn, WRValue& retVal, void* usr)
{
  if ( argn == 1)
  {
    int value = analogRead(argv[0].asInt());
    wr_makeInt( &retVal, value);          // put the value in the return package
  }
}

void lcd_begin( WRValue* stackTop, const int argn, WRContext* c)
{
  if( argn == 2)
  {
    int columns = stackTop[-2].asInt(); // first argument
    int rows    = stackTop[-1].asInt(); // second argument
    int status = lcd.begin( columns, rows);
    wr_makeInt( stackTop, status);
  }
}

void lcd_setCursor( WRValue* stackTop, const int argn, WRContext* c)
{
  if( argn == 2)
  {
    int column = stackTop[-2].asInt(); // first argument
    int row    = stackTop[-1].asInt(); // second argument
    lcd.setCursor( column, row);
  }
}

void lcd_print( WRValue* stackTop, const int argn, WRContext* c)
{
  if( argn == 1)
  {
    char buf[60];
    int n = lcd.print( stackTop[-1].asString(buf, sizeof(buf)));
    wr_makeInt( stackTop, n);
  }
}

double FrancoisVieteFormula()
{
  double two = 2.0;
  double s = 0.0;
  double t = 1.0;

  for( int i = 0; i < 10; i++)
  {
    double r = s + two;
    s = sqrt(r);
    t *= s / two;
  }
  double pi = two / t;
  return( pi);
}

The sketch in Wokwi:

When the simulation is running, click on the NTC module to change the settings.

1 Like

I2C Scanner in wrench: wrench_I2CScanner.ino - Wokwi Arduino and ESP32 Simulator

This is good stuff. I think I'll incorporate your lib functions into the wrench release, if that's okay with you (MIT License)

The reason I haven't yet is they can't be built into the standard wrench.cpp/h because no PC analog. But I think it's okay to say "include arduino_lib.cpp in your project for these library functions" and it will still be easy to integrate.

I'd like to add your name to the credits of course if you are okay with this how would you like it to appear?

This is only one of the many possibilities of your interpreter: running a "Arduino"-alike interpreter on a Arduino board.
It can provide a secure environment, for example when only two pins may be used by a client. It can also be an easy way to update a project via a webpage.

For a Arduino Uno, separate modules are needed. Maybe with a #define to turn some things on.
For example: As soon as something is included in the sketch (#include <Wire.h>) then memory is allocated since they are often not pure C++ classes.
If I include a library in the sketch, then I want to do something to include that in wrench.
If I don't use that library in the sketch, then I do not want all the wr_registerFunction() and wr_registerLibraryFunction() and I don't want the callback functions either.

Can you give these a thought ?

  • Shall I use servo1::write(), servo2::write(), and so on. Declare everything for a certain number of servo motors ?
  • I want to do some timing with millis(), however, then I need "unsigned long". With millis(), I can do everything that I want, a timing-library limits the possibilities. Shall I pick a timing library and make a interface to that ?
  • You use tabs instead of spaces. Can you use spaces ? Tabs are annoying and never the same. Github shows the source code with a tab, and the browser makes 8 spaces of that. I use two spaces. When I upload a mix of your indents and my indents on this forum, then it is a big mess.
  • How about Serial::print() :rofl: That is against what you had in mind :scream:

I agree. I noticed that including Wire.h caused memory and flash space to be allocated. This came as a bit of a shock to me :slight_smile:
I don't have the best idea yet but I have included an arduino_lib.c file that is the beginings of a library. I'll have to actually use it to see how well it integrates.

Shall I use servo1::write(), servo2::write(), and so on. Declare everything for a certain number of servo motors ?

I think servo::write( <servo #>, <val> ); makes more sense

I want to do some timing with millis(), however, then I need "unsigned long". With millis(), I can do everything that I want, a timing-library limits the possibilities. Shall I pick a timing library and make a interface to that ?

I think that would be the best approach. wrench is meant to be modular, only loading what you need dynamically. So something like wr_loadTimerLibXXX(); to get the specific methods for library XXX installed I think is the right approach.
That way the linker knows to only include that code if the call is made, and leave it out otherwise.

You use tabs instead of spaces. Can you use spaces

Sorry to be on the wrong side of the religious debate but I use tabs and set them to be 4 spaces in all of my editors.

How about Serial::print() :rofl: That is against what you had in mind :scream:

Add whatever you want! printing is always a hairy affair. I provided str::sprintf() to help with this, but very intentionally did NOT provide any default behavior. In the past I've provided a callback to emit text, maybe I'll add functionality to wrench.

Major update, I'll probably start a new topic if I can think of a reason the general public would be interested. Lots of improvements as I work with it in my actual project, and I knocked another 3k off the image size, it fits into 3/4ths of an Uno Mini now :slight_smile: leaving plenty of room for images.

2.00 adds two opcodes (casting to float and int) and a CRC to the bytecode, so it is NOT backwards compatible with 1.xx

I know what a hassle this can be an plan to never do it again, keeping the bytecode format constant across releases is crucial.

1 Like

I updated the floating point test, and there is a parsing bug in version 2.00.
This is not properly parsed: r = s + two;
This is my workaround: r = s; r += two;

Yes you are right, I have tracked down the cause and it's not immediately apparent what's wrong. Has to do with the optimized loading of a float or 32-bit value. I am busy with dayjob but I'll fix this bug asap