Introducing Justina: 'Just an Interpreter for Arduino'

by herwig9820 in Circuits > Arduino

1836 Views, 20 Favorites, 0 Comments

Introducing Justina: 'Just an Interpreter for Arduino'

Picture0-C.png

A fully documented, structured, easy-to-use programming language for Arduino

Justina (short for 'Just an Interpreter for Arduino') was developed and built around a few objectives.

  • On top of the list: simplicity for the user. Justina is a structured symbolic language, and it’s easy to learn.
  • Equally important: Justina was built with Arduino in mind - more specifically, 32-bit Arduino’s: boards with a SAMD processor (like the nano 33 IoT), the nano ESP32 and the nano RP2040.
  • High quality and complete documentation: the Justina user manual completely covers functionality, including more technical aspects like integration with c++, system callbacks, examples and so on

Justina does not impose any requirements or restrictions related to hardware (pin assignments, interrupts, timers,... - it does not use any), nor does it need to have any knowledge about it for proper operation. The Justina syntax has been kept as simple as possible. A program consists of statements. A statement either consists of

  • a single expression (always yielding a result).
  • a command, starting with a keyword, optionally followed by a list of arguments (expressions). Such a statement is called a command, because it ‘does’ something without actually calculating a result. Arguments are separated by comma's; the argument list is not surrounded by parentheses.


The ideal tool for quick prototyping

Because Justina is an interpreted language, a Justina program is not compiled into machine language but it is parsed into tokens before execution. Parsing is a fast process, which makes Justina the ideal prototyping tool. Once it is installed as an Arduino library, call Justina from within an Arduino C++ program and you will have the Justina interpreter ready to receive commands, evaluate expressions and execute Justina programs. You can enter statements directly in the command line of the Arduino IDE (the Serial monitor by default, or a Terminal app (e.g., YAT), a TCP IP client,...) and they will immediately get executed, without any programming.

Excited to get hands-on with Justina? 

Feel free to jump ahead to Step 6 ‘Getting started with Justina: a simple setup guide’ for immediate practical experience. However, don’t miss out on the valuable insights in the preceding sections - they lay the groundwork for a smooth and informed journey with Justina.

Supplies

Arduino nano: 33Iot or ESP32 or RP2040

Optionally, for some of the examples included in the library:

  • a few LEDs, resistors and pushbuttons
  • 128 x 64 OLED display (SPI: VMA437 or similar; I2C: VMA438 or similar)

Justina: a Quick Overview

Picture1.png
Picture2.png

A few highlights

  • More than 250 built-in functions, commands and operators, 70+ predefined symbolic constants.
  • More than 30 functions directly targeting Arduino IO ports and memory, including some new.
  • Operator set includes relational, logical, bitwise operators, compound assignment operators, pre- and postfix increment operators.
  • Integrated TCP/IP connection setup / maintenance function library.
  • Two angle modes: radians and degrees.
  • Scalar and array variables.
  • Floating-point, integer and string data types.
  • Perform integer arithmetic and bitwise operations in decimal or hexadecimal number format.
  • Display settings define how to display calculation results: output width, number of digits / decimals to display, alignment, base (decimal, hex), …
  • Input and output: Justina reads data from / writes data to multiple input and output devices (connected via Serial, TCP IP, SPI, I2C...).
  • Switch the console from the default (typically Serial) to another input or output device (for instance, switch console output to an OLED screen).
  • With an SD card breakout board connected via SPI, Justina creates, reads, writes, copies SD card files, ...
  • Send and receive files to / from any connected IO device capable to receive / send files.
  • In Justina, input and output commands work with argument lists: for instance, with only one statement, you can read a properly formatted text line from a terminal or an SD card file and parse its contents into a series of variables.

The console

Multiple I/O devices can connect simultaneously to Justina (see the cover photo) but one of them is always designated as console (by default, the Serial Terminal connected to Justina via USB - typically the Arduino IDE Serial Monitor). Command setConsole changes the console to any suitable IO device, e.g., a TCP/IP terminal).

Justina will process statements typed in the console command line (commands, expressions, calls to Justina user functions, ...) and will send system output and error messages back to the console.

User input is first parsed, then echoed (pretty printed) after the 'Justina>' prompt - providing a history of what has been entered - and executed.

Multiple statements can be entered on a single line, separated by semicolons. If at least one statement is a 'simple' expression (not a command), the result of the (last) expression is printed on the console as well. This is useful to use Justina as a calculator.

When execution ends, the Justina prompt will be printed again, indicating that Justina is ready to receive new user input.

Example: the statements below will set the console display width for calculation results to 40 characters wide (the default is 64) and set the angle mode to degrees. Arduino pin 17 is then set as an output pin, before a HIGH value is written to the pin. Finally, the cosine of 60° is calculated.

dispWidth 40; angleMode DEGREES;               // 2 commands
pinMode( 17, OUTPUT); digitalWrite(17, HIGH); // 2 expressions
cos(60); // 1 expression

Programming

Picture3.png


  • Call Justina functions with both mandatory and optional parameters. Scalar and array arguments are both accepted.
  • When calling a function, variables (scalar and array) are passed by reference, while literal / predefined constants and evaluated expressions are passed by value.
  • Variables or constants declared within a program can be global (accessible throughout the Justina program), local (accessible only within a Justina function), or static (accessible within one Justina function, with the value preserved between calls).
  • Variables not declared within a program but by a user from the command line are called user variables (or user constants).
  • Programs have access to user variables, and users have access to global program variables from the command line. User variables preserve their values when a program is cleared or another program is loaded.
  • Parsing and execution errors are clearly indicated, with error numbers identifying the nature of the error.
  • Error trapping, if enabled, ensures that an error will not terminate a program; instead, the error can be handled in code (either in the procedure where the error occurred or in a ‘caller’ procedure). It’s even possible to trap an error in the command line.
  • If Justina program files are available on an SD card (assuming an SD card board is connected), they can be directly read and parsed from there. Alternatively, your computer can send Justina program files to an Arduino, provided you use a Terminal app that can send files (the Arduino IDE does not provide that functionality; however, a very good—and free—choice is YAT Terminal).

Program editing

You'll need a text editor to write and edit your programs. Any text editor, such as Notepad, will work, but you might consider using Notepad++, because a specific 'User Defined Language' (UDL) file for Justina is available to provide Justina syntax highlighting.

Debugging and Tracing

Picture4.png

When a program is stopped (either by execution of the ‘stop’ command, by user intervention or by an active breakpoint) debug mode is entered. You can then single step the program, execute statements until the end of a loop, a next breakpoint…

Breakpoints can be activated based on a trigger expression or a hit count. You can also include a list of ‘view expressions’ for each breakpoint, and Justina will automatically trace specific variables or even expressions, letting you watch their values change as you single step through the program or a breakpoint is hit.

While a procedure is stopped in debug mode, you can also manually review the procedure’s local and static variable contents or view the call stack.

Integration With C++ - Part 1

Picture5.png

System callbacks

If enabled, system callbacks allow the Arduino program to perform periodic housekeeping tasks beyond the control of Justina (e.g., maintaining a TCP connection, producing a beep when an error is encountered, detecting a key press requesting to abort or stop a Justina program...).

For that purpose, at regular intervals, Justina calls a custom (user-written) C++ function (a 'callback' procedure) that is part of the user program (.ino file) and performs the required custom actions. A set of system flags passes information back and forth between Justina and this custom C++ function. For instance, a system flag will inform the custom C++ function that an error occurred. It is up to that custom function to turn an error LED on, produce a beep, ... At the same time, if the custom function detected a key press for a button labeled 'stop', it will set a system flag requesting Justina to stop execution and enter debug mode.

Using this approach, Justina does not need to have any knowledge about pushbuttons, LEDs, IO pins, timers, or other hardware used.

Justina will call this custom C++ function when at least 100 milliseconds have passed since the previous call. If Justina is currently not idle, calls are made in between individual statements being executed (or parsed).

Justina functions that time out, such as wait(time) - the Justina equivalent of the Arduino delay() function - or the findUntil() function will not interrupt these regular calls (in contrast to the Standard Arduino delay() or findUntil() functions) and can safely be used.

Integration With C++ - Part 2

Picture6.png
Picture7.png

User callbacks: user functions written in C++

Time-critical user routines, functions targeting specific hardware and functions extending Justina functionality in general can be written in C++, given an alias and 'registered' with Justina (using a standard mechanism), specifying the alias, start address of the procedure, minimum and maximum number of arguments and return type. From then onward, these user C++ functions can be called just like any other Justina function, with the same syntax, using the alias as function name and passing scalar or array variables as arguments.

Getting Started With Justina: a Simple Setup Guide

Installing the Justina library

Start by installing the Justina library, named ‘ Justina interpreter ’, from the Arduino library manager. In the Arduino IDE:

  • select 'Tools -> Manage Libraries' and look for 'Justina interpreter' (filter the library list by "Justina"). Click 'Install', to install library 'Justina interpreter'.
  • select the correct board and port. Justina has been tested with the Arduino nano 33 IoT, the Arduino nano RP2040 and the Arduino nano ESP32. My preference: the nano ESP32 board. Compilation is relatively fast (much faster than with the nano RP2040 board - something you will get to appreciate if you have multiple test-debug-correct-compile cycles) and it has a large memory (larger than the 32K RAM of the nano 33 IoT).

Alternatively, you can also install the Justina library from GitHub:

https://github.com/Herwig9820/Justina_interpreter

Starting Justina

Justina must be called from your Arduino (C++) sketch.

This involves the following actions:

  • creating a Justina object
  • optional: adding additional IO channels for use by Justina, such as OLED displays or a TCP/IP connection
  • for TCP/IP connections: specifying WiFi SSID and password, server address and port, ...
  • starting Justina

Additionally, your sketch may or may not specify

  • a system callback procedure (see a previous step)
  • user callback procedures (see previous step)

The Justina library contains examples covering all these cases.

Now let's try out a very easy example: see the code below. This will set the Serial Monitor (or any other Serial Terminal connected via USB) as the console (which is the default).

#include "Justina.h"

Justina justina; // create Justina_interpreter object with default values

// -------------------------------
// * Arduino setup() routine *
// -------------------------------
void setup() {
Serial.begin(115200);
delay(5000);
justina.begin(); // run interpreter (control will stay there until you quit) Justina)
}

// ------------------------------
// * Arduino loop() routine *
// ------------------------------
void loop() {
// empty loop()
}


No need to copy this program: it is provided as one of the Justina library examples.

  • In the Arduino IDE, select 'File -> Examples -> (scroll down to section 'Examples from Custom Libraries') Justina_interpreter -> Justina_easy'

A sketch 'Justina_easy.ino' will open. It contains the same code as pictured above (and a lot of comments, too).

  • Compile and upload your sketch.

When done, your Serial Monitor will print a Justina copyright and version text, followed by the Justina prompt.

That's it!

Control is now within Justina (and will stay there until you execute the 'quit' command).

Your First Steps With Justina

To begin your journey with Justina, we will type a few statements in the command line of the Serial monitor and check out the results.

To begin, type the following command and press ENTER :

var subTotal = 0, total = 0.;

Command var is a non-executable command: it defines 2 variables and initialises them both to to zero. This initialization is done during parsing. But there's a difference: the data type of 'subTotal' is set to integer, whereas the data type of 'total' is set to floating point (values containing a decimal point or an exponent are considered floating point numbers). Arguments are always separated by a comma. The statement is terminated by an (optional) semicolon.

Now type

sin(PI / 2); total = 5 + 7 + (subTotal = 6 + 2);

This time, the line entered contains 2 statements (both being simple expressions, not commands), separated by a (mandatory) semicolon.

If statements entered in the command line contain at least one 'simple' expression (not a command), then Justina will print the result of the last expression to the Serial Monitor (command statements do not produce a result). This is very useful if you want to use Justina as a scientific calculator .

In this example, the last (second) expression contains a sub-expression between parentheses. The result of this sub-expression will first be assigned to variable 'subTotal', which will now store integer value '8'. Then, this value will be added to the other values of the main expression and the result will be assigned to variable 'total' (note that this will change the data type of variable 'total' to integer). Finally, this result ('20') will be printed right-aligned within the set display width (specific commands are available to change these settings).

Because in this example, the result of the first expression is not assigned to a variable, its result will be lost.

Let's rearrange the second statement into 2 separate statements: type

subTotal = 6 + 2; total = subTotal + 5 + 7;

Obviously, this leads to the same result as previous example.

To create a constant (a variable initialized during parsing with a value that can not be changed afterwards), use the const keyword instead of var.

Example: type

const logoFile = "/justina/images/jus_logo.jpg";

Constant variable logoFile will be initialized as a string, containing the path to an SD card file (supposing an SD card board is connected).

Let's create one more variable: an array. Type

var thisIsAnArray(3, 2, 10) = 0.

This defines a 60 element floating point array, initialized to zero. In contrast to scalar variables, the data type of an array (or, any of its elements) is fixed. But numeric values assigned to a numeric array will be converted to the data type of the array.

To list all currently defined variables, type

listVars;

Justina will print a list of all user variables with data type, current value (or array element count) etc.

Now, type the following two lines

var i = 0;
for i = 1, 100; printLine CONSOLE, fmt(i, 3, 1, DEC, FMT_NONE), fmt(i*i, 6); if i == 50; break; end; end;

This ' immediate mode program ' contains a number of control statements: they control the order in which other statements are executed.

  • The for and outer end commands define a control structure that executes the statements within it multiple times, controlled by variable 'i' (a 'loop').
  • The if and inner end form a control structure containing a test clause. If the test results is 'true', the inner statements are executed.
  • The break statement exits the for...end control structure.
  • Command 'printLine CONSOLE, fmt(i, 4, 2, DEC, FMT_NONE), fmt(i*i, 6, 1)' prints both the value of 'i' and its squared value as integers, in decimal notation, without formatting flags applied, right-aligned, in two print fields, 4 and 6 columns wide, respectively. The first value ( i ) is printed with minimum 2 digits.

Please refer to the Justina user manual for a detailed description of all the options.

Note that this command will typically be used from inside a 'real' Justina program.

Argument 'CONSOLE' (which is optional) directs output to the Serial Monitor (because by default, the Serial Monitor is set as the console).

Using the same command, you could print to additional I/O devices, if defined (a TCP/IP terminal, an SPI or I2C OLED display etc.).

The printLine command considers the output device it is printing to, as a terminal: printing starts in the most-left column.

Because in this last example, the statements entered do not contain any 'simple' expression (only command statements), a 'last result' is not printed when execution stops.

Where Do You Go From Here ?

Picture8.png
Picture9.png

Exploring Justina further can be both rewarding and enjoyable. The library comes with a number of instructive examples, from easy to more advanced. The user manual is a very helpful resource, offering detailed guidance on Justina’s commands and functions. When you’re ready, you might find the sections on extending I/O, SD cards, programming, and creating your own Justina library extensions particularly useful. You might even discover a new passion for tinkering with Justina.

User manual

A link to the user manual on GitHub:

https://github.com/Herwig9820/Justina_interpreter/blob/master/docs/Justina%20user%20manual.pdf

A copy of the manual is attached as well.

Arduino C++ examples

A number of C++ example sketches are provided in Justina library folder 'examples'. These examples demonstrate topics such as how to call Justina from within your C++ sketch; using system callbacks to regularly execute specific tasks in the background (e.g., TCP/IP connection maintenance).

Other examples demonstrate how to write Justina user C++ functions, how to put them in a Justina user C++ 'library' file and how to add additional IO channels, such as OLED displays or a TCP/IP connection, next to Serial.

To load a C++ example: in the Arduino IDE, select 'File -> Examples -> (scroll down to section 'Examples from Custom Libraries') Justina_interpreter and select the example you want to load.

The example sketches are listed, with a short description, in the User Manual, Appendix C: Examples. Each sketch itself contains quite extensive comments as well.


Justina language examples

Justina library folder 'extras/Justina_language_examples' contains a number of Justina language example files (Justina programs). These text files obey the 8.3 file format, to make them compatible with the (optional) Arduino SD card file system. Also, they all have the '.jus' extension: opening these files in Notepad++ will automatically invoke Justina language highlighting (if the Justina language extension is installed).

Examples include an 'auto start' program, a recursive method to calculate factorials, setting up extra IO channels, reading and writing SD card files and more.

One example involves setting up an Arduino to function as a HTTP web server. When a browser establishes a connection to this server, it is served a webpage that features a fully operational scientific calculator (second figure, above).