I have been hankering after a
plugin that takes instructions in JavaScript. At home because of Covid-19 lockdown, I decided to learn C++ and about
plugin programming... thank you Mike @Rasbats for help getting me going.
I now have a working prototype of my JavaScript plugin, which has a built-in JavaScript
engine implementing ECMAScript E5.1 together with some features of E6 and E7. I introduce it here. This is rather a long post as there is a lot to say.
The plugin presents a console window comprising a script pane and a results pane and some buttons. You enter your JavaScript in the script pane (or load it from a file) and click on the Run button. The script is compiled into byte code and executed and any result is displayed. At its simplest, enter, say
and the result 4 is displayed. But you could also enter, say
Code:
function fib(n) {
if (n == 0) { return 0; }
if (n == 1) { return 1; }
return fib(n-1) + fib(n-2);
}
function build(n) {
var res = [];
for (i = 0; i < n; i++) {
res.push(fib(i));
}
return(res.join(' '));
}
print("Fibonacci says: ", build(20), “\n");
Which displays
Fibonacci says: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
This illustrates how functions can be defined and called, including recursively. So we have a super calculator!
I have been developing interfaces (APIs) to
OpenCPN and it is here that the fun starts. With my first, the following statement takes the supplied
NMEA sentence, adds a checksum (replacing any existing one) and pushes it out over the
OpenCPN connections
Code:
OCPNpushNMEA(“$OCRMB,A,0.000,L,,Yarmouth,5030.530,N,00120.030,W,
15.386,82.924,0.000,5030.530,S,00120.030,E,V");
A challenge has been how to arrange for the processing of OpenCPN
events. I have developed a way of specifying a function to handle a particular event, so we can write, for example
Code:
OCPNonNMEAsentence(handleNMEA);
function handleNMEA(result){
if(result.OK){
print(“Received: “, result.value, “\n”);
}
else print(“Bad checksum\n”);
}
This listens out for the next
NMEA sentence and displays it on receipt, if the checksum is OK.
Structures and methods
JavaScript supports the use of structures and methods. I have created a structure
position, which allows me to write things like:
Code:
mypos = new position(50.234, 3.231);
// let’s change the longitude
mypos.value.longitude = -1.674;
Originally, I did not have the ‘value’ component, but I have added it to align with SignalK usage.
I have added some methods, such that
Code:
print(“My position is “, mypos.formatted(), ‘\n”);
// displays “My position is 50º 14.040’N 001º 40.440’W”
mypos.NMEA() returns the string
5014.04,N,140.44,W, which is the very odd way positions are represented in NMEA sentences.
NMEAdecode(string, n) decodes the nth position in an NMEA sentence. So
Code:
sentence = "$OCRMB,A,0.000,L,,UK-S:Y,5030.530,N,00120.030,W,15.386,82.924,
0.000,5030.530,S,00120.030,E,V,A*69";
mypos.NMEAdecode(sentence,1); // decodes NMEA sentence
print(mypos.formatted(), “\n");
// displays 50º 30.530’N 001º 20.030’W
A simple application
As an illustrative application, I note that @arnolddemaa was
having problems capturing magnetic variation from HDG sentences and inserting into RMC sentences. Here is a solution which captures the variation from HDG sentences and inserts it into any RMC sentences that do not already have the variation. It changes the RMC sender identity so that the originals can be blocked by OpenCPN filtering. It works by splitting the sentence into an array of fields. Note how it ends by setting up to process the next NMEA sentence.
Code:
// insert magnetic variation into RMC sentence
var vardegs = ""; // where we will save the latest variation
var varEW = "";
OCPNonNMEAsentence(processNMEA);
function processNMEA(result){
if (result.OK){
sentence = result.value;
switch (sentence.slice(3,6)){
case “HDG:" // capture variation
splut = sentence.split(",");
vardegs = splut[4]; varEW = splut[5];
break;
case "RMC":
splut = sentence.split(",");
if (splut[10] == "") // if no variation already {
splut[10] = vardegs; splut[11] = varEW;
splut[0] = “$JSRMC”;
}
result = splut.join(“,"); // put sentence together
OCPNpushNMEA(result);
break;
}
}
OCPNonNMEAsentence(processNMEA);
};
OpenCPN Messaging
I have implemented APIs to handle OpenCPN messages. Different messages can be directed to message-specific functions. For example:
Code:
//request route list
routeRequest = '{"mode": "Not track"}' // JSON needed to get route
OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", routeRequest);
function handleRL(routeListJS){ //handle receipt of the route list
routeList = JSON.parse(routeListJS);
// notice how easy it is to parse the JSON into a structure
// for illustration, here we extract the GUID of the first route
firstGUID = routeList[0].GUID;
}
Probing OpenCPN
I have found the plugin an excellent way of probing OpenCPN functionality, particularly as I can evolve the script in the light of what I get, iteratively. Wondering what a route list looks like? This will show you:
Code:
OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", JSON.stringify({"mode": "Not track”}));
function handleRL(routeListJS){ // handle route list response
print(routeListJS, “\n”);
}
A more complex application
My own particular interest has been to develop a way of arranging that mobile devices running
iNavx maintain an up-to-the-moment copy of an OpenCPN active route and shadow OpenCPN in a way that they will continue navigating the route were OpenCPN to fail. I have described that need in
another post here. An advantage of
iNavX is that it displays navigational information, including estimated times en-route, in a way optimised for
iPad or small
phone screens. The requirement is that
- the active route waypoints are sent out as a series of WPL sentences.
- These are associated into a route by a series of RTE sentences. Because of the 80 character limit, this takes a sequence of sentences using ’n of m’ notation. How many it takes depends on the length of the route and waypoint names.
- A BOD sentence is required to allow iNavX to navigate from the position where the next waypoint became active to the active waypoint.
- I have found that OpenCPN truncates the waypoint name in RMB sentences to 6 characters. Presumably, this is to allow for certain aged autopilots. This causes confusion if the active waypoint name is longer. I check RMB sentences for a truncated name and replace it with the full waypoint name.
The first step is to obtain the active route GUID. It turns out there is a bug in OpenCPN v5 whereby the returned GUID is actually the route name and not the GUID. This has been fixed in OpenCPN v5.1.605. So my script checks to see whether the route name was returned instead of the GUID and, if so, it has to fetch the full list of routes and look to see which is the active one. This extra
work is avoided if the GUID is returned correctly.
I have a script that works as I require. It is too long to include in this post but it
can be viewed here. This script includes many comments to aid understanding and I have left in, but commented out, some of the print statements used during its development. This shows key points in its evolution
.
Road map for future development
So far, I have only implemented the APIs needed to achieve my application above. I would like to hear of other possible applications so I can consider further APIs.
The plugin is not yet ready for alpha testing but I would be interested to
work with others in a pre-alpha phase to develop ideas. This pre-alpha phase will be MacOS only as I use Xcode for debugging. I propose we use a Slack workspace I set up to liaise with Mike. If you would like to join in, please contact me by private message.
I anticipate developments will include:
- Addition of further APIs as needed
- Documentation and a user guide
- Making the scripting window more programmer friendly. At present, it knows nothing of tabs, indents and braces. For other than the simplest script, I am using a JavaScript-aware editor (BBEdit in my case) and paste the scripts into the script window.
- Better resilience. At present there is no protection against a script loop. while(1); hangs OpenCPN!
- Implementing the JavaScript require() function, which is like a C++ #include to allow loading of pre-defined functions, objects, and methods.
- Running without the console window visible
- Tidier and more consistent error reporting, even when the console is hidden
- ‘Canned’ scripts that start automatically
- At present, if you want to do separate tasks, you would need to combine them into a single script. I have ideas about running multiple independent scripts.
- I do not use SignalK but note its potential. I am interested in input from SignalK users to keep developments SignalK friendly.
Other suggestions?
Now to catch up on other things neglected over the past six weeks!