Subsections of Scripting
MecScript Language
MecScript is a statically-typed, heap-less, C-style scripting language for 32-bit embedded systems. Source files (.mec) compile to a compact bytecode binary (.mbin) that runs on a stack-based virtual machine inside the host device.
1. Program Structure
There is no main function. Top-level statements execute in source order when the script runs.
System:: is the namespace host (native) functions are called through by default. A host can put its natives in a namespace of its own instead. See Native functions.
Functions and classes may be declared at the top level. They can be referenced before their declaration appears in the file (see Forward references).
2. Comments
MecScript supports C-style single line and block comments.
3. Types
MecScript is a statically typed language. All data types are known at compile time, making execution faster and safer.
| Keyword | Alias | Meaning | Width |
|---|---|---|---|
void | no value (return type) | - | |
bool | true / false | 1 byte | |
char | s8 | signed 8-bit integer | 1 byte |
byte | u8 | unsigned 8-bit integer | 1 byte |
short | s16 | signed 16-bit integer | 2 byte |
ushort | u16 | unsigned 16-bit integer | 2 byte |
int | s32 | signed 32-bit integer | 4 byte |
uint | u32 | unsigned 32-bit integer | 4 byte |
float | f32 | 32-bit IEEE-754 | 4 byte |
string | immutable text constant | ref |
The alias column is just another way to declare the same keyword: s8 and char compile to exactly the same type, so they mix freely and error messages always report the char/byte/… name.
Aliases are provided for convenience as they are a little more descriptive than the classical type names.
char and byte are numeric types, not a distinct character type. Individual character literals are not supported, so an integer must be used.
string is a reference to a compile-time constant. String variables and string native parameters can only ever hold a literal or another string constant. For mutable text, use a byte array (see Strings).
4. Literals
null, NULL, and nil are all accepted and evaluate to integer 0.
String literals use double quotes. There are no escape sequences: "\n" is a backslash followed by n, not a newline.
5. Variables
All variables are zero-initialized if no initializer is given.
Scope
- Variables declared at the top level are globals.
- Variables declared inside a function or block are locals.
- Blocks (
{ ... }) introduce a new scope. A local is visible from its declaration to the end of its enclosing block.
const
const marks a variable read-only after its initializer runs. Writing to it later is a compile error.
Naming Style
MecScript enforces no naming convention. One common pattern is UpperCamelCase for functions and global variables and lowerCamelCase for locals and parameters, which makes a name’s scope visible at a glance and matches how the native reference set is written. The compiler accepts any style, and the choice is entirely yours.
6. Operators
All basic mathematical and bitwise operations are supported.
Arithmetic
Important
Integer division and modulus by zero halt the VM (vmDivisionByZero). Float division by zero also halts.
Bitwise
Comparison
A comparison always produces a bool.
Logical
Assignment
There is no %=, <<=, or >>=.
Increment / Decrement
Both prefix and postfix forms work on a variable:
Narrow types wrap at their own width:
Ternary
Behaves like a single line if/else statement.
Precedence
From tightest to loosest binding:
.[]()(member, index, call)!~++--(unary)*/%+-&|^<<>><><=>===!=&&||?:(ternary)=+=-=*=/=(assignment)
Note that the bitwise and shift operators sit at the same level as + and -. Parenthesize when mixing them with arithmetic.
7. Type Conversions
Numeric types convert implicitly across signed, unsigned, and float categories as needed by an assignment, argument, or operator. There is no explicit cast syntax.
There is no integer overflow or wraparound detection. Arithmetic that exceeds a type’s range wraps silently.
8. Control Flow
if / else if / else
while
A while loop will continue as long as the condition is true.
There is no do/while.
for
A for loop has an initializer, condition, and update expression. The loop will continue as long as the condition is true.
All three clauses are optional. Any of them may be left empty, and for (;;) is an infinite loop.
break & continue
continuewill skip the rest of the loop body and continue to the next loop iteration.breakexits the loop immediately.
break and continue work in while and for loops.
switch
The controlling expression is an integer (float is rejected). case labels are integer literals and must be unique. default is optional. A case without a break falls through to the next case, as in C.
A case with no body falls straight into the next one, which is how several values share a single handler:
A case that has a body but no break runs its own body and then continues into the next case:
Info
switch statements are much more efficient than long if/else chains. They use a jump table to very quickly jump to the correct case label, rather than checking every entry for a match. The tradeoff is they produce more compiled binary size for large ranges.
A switch statement has to produce a jump table entry for every value between its lowest and highest case label value. This means that for large value ranges with a low number of case labels, the compiled output will be huge compared to if/else. The number of case labels has no effect on performance.
The compiler will output a warning if the number of case labels is less than half the value range.
9. Functions
- Scalar parameters are passed by value.
- A non-
voidfunction mustreturna value. - Calling with the wrong number of arguments is a compile error.
- Recursion is supported.
Forward References
A top-level function or class may be used before it is declared in the file. This allows mutual recursion:
Array Parameters
An array parameter is written T name[] or T *name (equivalent). The array size is not carried in the type itself, so the caller still passes the array by bare name plus an explicit length argument, and should not rely on count/len matching the array’s real size. A variable index used inside the function is checked at runtime against the caller’s actual array, the same as any other array (see “Bounds checking” in the Arrays section); only a native function’s own C++ implementation is unchecked, C snprintf-style.
Passing a non-array where an array parameter is expected, or an array of the wrong element type, is a compile error. So is giving the parameter a size (int values[4]).
Inside the function the parameter is used exactly like an array: index it, or pass it on by name to another array parameter, script or native. It cannot be used as a value on its own.
Class parameters
See Classes.
10. Arrays
- Array size is fixed at compile time.
- If an initializer list is present, its length must match the declared size exactly.
int a[3] = {1, 2};is a compile error. - Without an initializer, every element is zero.
Indexing and assignment
Packed storage
char/byte arrays pack 4 elements per 4-byte slot; short/ushort arrays pack 2 per slot. This is transparent to the script; index them normally.
Bounds checking
A literal out-of-range index is a compile error, including a negative one:
A variable or computed index is checked at runtime instead. An out-of-range access halts the VM with vmArrayIndexOutOfBounds rather than reading or writing whatever happens to sit next to the array:
This covers array parameters too: a function indexing a T name[] parameter with a variable index is checked against the size of whatever array the caller actually passed in, even through several levels of forwarding. It also covers an array field declared inside a class, however it’s reached: directly, through a class-typed parameter, or through a composed/embedded instance. It never applies inside a native function’s own implementation, which reads and writes the buffer directly in host C++ with no VM involvement at all.
Bare array references
An array name used without an index has no value. It cannot be assigned to a scalar, returned as a scalar, or passed as a scalar argument. Pass it only to an array parameter (with a length) or index it.
11. Strings
String literals are immutable compile-time constants. Use them directly with the print natives:
For text you need to build or modify at runtime, use a byte buffer and the string natives. Every string native takes an explicit capacity, C snprintf-style. Nothing grows a buffer for you.
Notes and limits:
StrCopy/StrAppendtake astringconstant as the source, not anotherbyte[]buffer.- A freshly declared buffer is zero-filled, so
StrLengthon an untouched buffer is 0. - Content that does not fit the capacity is truncated and null-terminated.
12. Classes
Classes are supported for advanced data structures.
Fields
Declared in the class body. Each instance gets its own copy.
Methods
- Inside a method,
this.fieldand a barefieldname both refer to the current instance’s field. - A method can call a sibling method on the same instance with
this.method().
Constructors
ClassName(params) { ... }. A class has at most one constructor. It runs when an instance is declared with an argument list:
Fields are always zero-initialized first, before the constructor body runs. A class with no constructor is declared without parentheses:
Declaring an instance of a class that has a constructor without an argument list compiles, but produces a warning.
Destructors
~ClassName() { ... }. Runs automatically when the instance goes out of scope:
- A local instance is destroyed at the end of its enclosing block.
- Multiple instances in the same scope are destroyed in reverse declaration order (LIFO).
- Global instances are destroyed once, at script end, in reverse declaration order.
Passing instances
Pass an instance to a function or method by reference with ClassName *param. The callee can call methods on it and read or write its fields, including compound assignment.
Passing an instance of the wrong class, or a non-instance, is a compile error. A bare instance name (no ., no method call, not passed to a class parameter) has no value and cannot be used as one.
Class-typed fields (composition)
A field may itself be a class instance. The embedded instance is laid out inline in its owner and reached with a chain of .:
- Nesting is unlimited:
a.b.c.xresolves as long as each step names a class-typed field. - Compound assignment works through the chain:
o.inner.value += 50;. - An embedded instance can be passed by reference like any other instance:
take(o.inner);wheretaketakesInner *i. - When an instance is created, each embedded field is zero-initialized and its field-default initializers run, outermost first.
- Destructors run automatically and in order: the owner’s destructor body first, then each embedded field’s destructor in reverse declaration order.
Limits:
- No member-initializer syntax. You cannot pass constructor arguments to an embedded field. Embedding a class whose constructor takes arguments is a compile error. A class with no constructor (or a parameterless one) is fine.
- No cycles. A class cannot contain itself, directly or indirectly (
class A { A a; }, orAholding aBthat holds anA). This is a compile error.
Not supported
- Inheritance. Every class is standalone. There is no subclassing.
- Class-typed return values. A function cannot return a class instance.
13. Namespaces
Namespaces are optional. They group top-level declarations under a name so they can be kept tidy and referred to explicitly. They have no runtime cost or effect: a namespaced global is still a plain global, and a namespaced function is still an ordinary function.
Refer to a member from outside with the :: scope operator:
- Unqualified access inside the block. Within
namespace Geometry { }, other members are visible without the prefix (area()can callgridSizeandPointdirectly). Names that don’t resolve inside the namespace fall back to the global scope. - Reopening. The same namespace name may be opened more than once, and the contents are merged.
- Forward references work across the whole file, exactly as they do at the top level.
- No nesting. A
namespacecannot be declared inside anothernamespace. - Native namespaces are reserved. A script cannot declare a namespace that natives were declared in, and
Systemis always off limits. See Native functions.
14. Preprocessor
Runs on the token stream before parsing. Two directives are supported.
#include
- Path is resolved relative to the including file first. If it is not found there, each include search directory given to the compiler (
MecCompile -I <dir>, seeTOOLCHAIN.md) is tried in order. Absolute paths are used as is. - Each file is included at most once, so diamond includes are safe.
- A circular include is a compile error, not a hang.
- A missing file is a compile error.
- Errors inside an included file are reported against that file’s own line numbers.
#define
Object-like macros only.
- A name is a macro only from its
#defineonward. - A macro body may reference an earlier macro, which is re-scanned and expanded.
- Self-referential and mutually-referential macros expand once and stop.
- An empty replacement is allowed and vanishes at the use site.
- Function-like macros (
#define SQ(x) ((x)*(x))) are a compile error. - There are no conditional directives (
#ifdef,#if,#endif,#undef). - A macro defined in an including file is visible inside included files.
15. Native functions
Native functions are implemented by the host application and called from a script. They are declared in a signature file that the build supplies to the compiler.
Calling convention
Every native must be called through the namespace it was declared in:
A bare Yield(10) is a compile error.
Declaration syntax
A declaration written outside any namespace goes into System:
Each declaration needs a unique [native N] id that maps to the host’s implementation. A native declared here but not resolved by the host fails at runtime (vmNativeFunctionNotResolved), not at compile time.
Namespaces
Wrap declarations in a namespace block to group them under a name of your own:
- The namespace only changes the name a script calls the native by. There is no runtime cost and no effect on the compiled binary, and the
[native N]id is still the only thing the host resolves against. - Namespaces cannot be nested, same as in a script.
- A namespace used by any native is reserved, so a script cannot declare one with the same name.
Systemis reserved whether or not anything is declared in it. - Only the namespace is reserved, not the names inside it. With
CAN::Readdeclared, a script is still free to declare its ownReadvariable or function, or aData::Readof its own.
Callback parameters
A native can take a script function as an argument. The parameter type is func, and the script passes a bare function name, no parentheses:
The parameter list after func is the signature the script function must have. It always returns void. The compiler checks the function’s parameter count, each parameter’s type, and whether it is an array, and reports a mismatch at the call site. A bare func with no parameter list accepts any void function; a wrong parameter count is then only caught when the host calls it (vmCallArgCountError).
An array parameter in a callback is host-owned memory that lives only for the duration of the call. Index it or pass it on to another array parameter as usual, and copy anything you want to keep into a script array before returning.
Reference set
| Signature | Purpose |
|---|---|
void SetError(int code) | Signal a recoverable error code to the host. |
void Print(string str) | Write a string, no newline. |
void PrintLine(string str) | Write a string and a newline. |
void PrintInt(int i) | Write an integer and a newline. |
void PrintFloat(float f) | Write a float and a newline. |
void PrintFormat(string str, float f) | Write a format string with one float (PrintFormat("v: %f", 3.14)). |
int StrLength(byte buf[], int capacity) | Length up to the null terminator or capacity. |
void StrCopy(byte dest[], int destCapacity, string src) | Copy a string constant into a buffer, truncating to fit. |
void StrAppend(byte dest[], int destCapacity, string src) | Append a string constant onto a buffer’s content. |
void IntToStr(byte dest[], int destCapacity, int value) | Format an integer as decimal text into a buffer. |
bool StrEquals(byte a[], int capA, byte b[], int capB) | Compare two buffers’ null-terminated contents. |
void PrintBuffer(byte buf[], int capacity) | Write a buffer’s null-terminated content. |
uint NowMs() | Host uptime in milliseconds. Wraps on overflow - compare with unsigned subtraction. |
uint NowUs() | Host uptime in microseconds. Wraps on overflow, typically much sooner than NowMs. |
void Yield(uint t) | Pause for t milliseconds. |
uint YieldUntil(uint lastTime, uint delay) | Fixed-period pause: sleeps until lastTime + delay, returns the new lastTime to pass back in next iteration. |
SetError
SetError(int) records an error code without halting the VM. The host reads it back after the run (MecVm::GetScriptError()). The last call wins. The default is 0.
16. Runtime Model and Limits
- No heap. Globals, locals, and call frames all live in one buffer supplied by the host. The VM never allocates at runtime.
- 64 KB bytecode ceiling. A single compiled binary, including everything pulled in by
#include, cannot exceed 65,535 bytes of bytecode. - Fixed stack. The host sets the stack size. Deep recursion or large local arrays can exhaust it (
vmStackOverflow). - Bounds checking. Stack overflow/underflow and out-of-range pointer dereferences are caught and halt the VM rather than corrupting memory. The host can disable this for targets that cannot afford the checks. A local, global, or array-parameter index that runs off the end of its array is also caught (
vmArrayIndexOutOfBounds); see “Bounds checking” under Arrays. - Division by zero halts the VM (
vmDivisionByZero) for integer and float operands. - No overflow detection. Arithmetic wraps silently.
- No exceptions. There is no
try/catch/throw. A genuine runtime error halts the VM. UseSetErrorfor recoverable conditions. - No string escape sequences.
VM status
A run ends with a status code. vmEnd is normal completion. Others include vmDivisionByZero, vmStackOverflow, vmStackUnderflow, vmPointerOutOfBounds, vmArrayIndexOutOfBounds, vmNativeFunctionNotResolved, and vmUnknownInstruction.
17. Complete Example
Output:
TCM System Functions
This is a reference for every native function callable from a MecScript script running on the TCM including:
- General-purpose functions under
System::for debugging and time management. - TCM-specific functions under
Runtime::,Device::,CAN::andOutput::for reading live device state, talking to the CAN bus and driving script-controlled outputs.
Quick Reference
Strings & Debugging (System::)
| Function | Signature | Purpose |
|---|---|---|
| System::Print | void Print(string str) | Debug-print a string, no newline |
| System::PrintLine | void PrintLine(string str) | Debug-print a string plus newline |
| System::PrintInt | void PrintInt(int i) | Debug-print an integer |
| System::PrintFloat | void PrintFloat(float f) | Debug-print a float |
| System::PrintFormat | void PrintFormat(string str, float f) | Debug-print a format string with one float |
| System::PrintBuffer | void PrintBuffer(byte buf[], int capacity) | Debug-print a byte buffer’s null-terminated content |
| System::StrLength | int StrLength(byte buf[], int capacity) | Length of a buffer’s content up to its null terminator |
| System::StrCopy | void StrCopy(byte dest[], int destCapacity, string src) | Copy a string constant into a buffer, truncating to fit |
| System::StrAppend | void StrAppend(byte dest[], int destCapacity, string src) | Append a string constant onto a buffer’s content, truncating to fit |
| System::IntToStr | void IntToStr(byte dest[], int destCapacity, int value) | Format an integer as decimal text into a buffer |
| System::StrEquals | bool StrEquals(byte a[], int capacityA, byte b[], int capacityB) | Compare two buffers’ null-terminated contents |
Time
| Function | Signature | Purpose |
|---|---|---|
| System::NowMs | uint NowMs() | Device uptime in milliseconds (wraps every ~49.7 days) |
| System::NowUs | uint NowUs() | Device uptime in microseconds (wraps every ~71.58 min) |
Script Yielding
| Function | Signature | Purpose |
|---|---|---|
| System::Yield | void Yield(uint t) | Pause the script for t milliseconds |
| System::YieldUntil | uint YieldUntil(uint lastTime, uint delay) | Fixed-period pause that returns the updated lastTime for the next call |
Runtime Channels (Runtime::)
| Function | Signature | Purpose |
|---|---|---|
| Runtime::Read | int Read(uint id) | Read a live channel value as an integer with the decimal point removed (123.4 reads as 1234) |
| Runtime::ReadReal | float ReadReal(uint id) | Read a live channel value as a float in real units |
| Runtime::Write | bool Write(uint id, int value) | Write a writeable channel value as an integer with the decimal point removed |
| Runtime::WriteReal | bool WriteReal(uint id, float value) | Write a writeable channel value as a float in real units |
CAN Bus (CAN::)
| Function | Signature | Purpose |
|---|---|---|
| CAN::SetupNode | bool SetupNode(uint node, bool enable, uint bitrateSelect, bool termination, bool listenOnly) | Enable/disable a CAN node and set its bitrate, termination and listen-only mode |
| CAN::Send | void Send(uint node, uint id, byte buffer[], int length) | Send a raw frame on CAN 1 or CAN 2 |
| CAN::Read | int Read(uint node, uint id, byte buffer[], int length) | Poll the latest received frame for a node/id pair |
| CAN::Subscribe | void Subscribe(uint node, uint idMatch, uint idMask, func(uint id, byte data[], int length) handler) | Register a callback for received frames matching an id/mask |
| CAN::Unsubscribe | void Unsubscribe(uint node, uint idMatch, uint idMask) | Cancel a subscription registered with the same node/match/mask |
| CAN::Poll | int Poll() | Fire the handlers for any frames received since the last call |
Output Control (Output::)
| Function | Signature | Purpose |
|---|---|---|
| Output::SetActiveLevel | void SetActiveLevel(byte outputId, int level) | Set a script output’s active polarity (0 = active-low, 1 = active-high) |
| Output::SetEffectiveResistance | void SetEffectiveResistance(byte outputId, float ohms) | Tell current control the load’s resistance |
| Output::SetFreq | void SetFreq(byte outputId, float frequency) | Set a script output’s PWM/current-chop frequency in Hz |
| Output::SetDuty | void SetDuty(byte outputId, float dutyCycle) | Drive a script output in PWM mode at a fixed duty (0-100) |
| Output::SetCurrent | void SetCurrent(byte outputId, float milliAmps) | Drive a script output in closed-loop current control |
Device Information (Device::)
| Function | Signature | Purpose |
|---|---|---|
| Device::ReadSerialNumber | uint ReadSerialNumber() | Read the device’s serial number |
| Device::ReadVendorKey | uint ReadVendorKey(int keyId) | Read one of two vendor keys (keyId 1 or 2), typically used to lock a script to a specific device |
Calling Convention
Every native function is called through its namespace.
Calling a native bare (Yield(10);, Poll();) or through the wrong namespace (System::Runtime::Read(1);) fails to compile. Function names are case-sensitive and must match the names in this manual exactly.
A callback handler passed to CAN::Subscribe (a func-typed parameter) is the one exception: it’s a bare script function name, not called through any namespace - see CAN Receive Callbacks.
Runtime ids (the uint id arguments to the Runtime:: functions) are plain numbers assigned by the device’s .mdef. Rather than hard-coding them, #include "mtc.mec" and use the RT_ names it defines - see Include Files.
Every CAN:: function takes a node as its first argument, numbered from zero: 0 is the bus MectriCal labels CAN 1 and 1 is CAN 2. CAN::Subscribe and CAN::Unsubscribe also accept 2 for both buses; everywhere else 2 is rejected. The include file provides CAN1, CAN2 and CAN_BOTH for these.
Included Device Definition Files
Device header file(s) ship along side the compiler, so a script can refer to channels, buses, bitrates, etc by name instead of by number. Include it at the top of the script:
The file name is the device’s name in lower case: mtc.mec for the MTC, tm16.mec for the TM16. The compiler already knows where the shipped headers live, so the plain file name works wherever the script itself is saved. Including the same file more than once (e.g. from two of your own include files) is harmless.
| Group | Names | Meaning |
|---|---|---|
| Constants | LOW, HIGH | 0 and 1, for Output::SetActiveLevel |
| CAN buses | CAN1, CAN2, CAN_BOTH | The node argument of the CAN:: functions (0, 1, 2) |
| CAN Bitrates | CAN_BITRATE_125K, CAN_BITRATE_250K, CAN_BITRATE_500K, CAN_BITRATE_1M, CAN_BITRATE_CUSTOM | The bitrateSelect argument of CAN::SetupNode |
| Runtime Channels | RT_ENGSPD, RT_GEAR, RT_BATTVOLTS, … | The Runtime ID of every channel, for the Runtime:: functions |
A runtime channel’s name is RT_ followed by its abbreviation in upper case (the “Engine Speed” channel becomes RT_ENGSPD). Each define carries the channel’s full label as a trailing comment, so the quickest way to find a channel is to open the header and search for the channel name.
You are free to define your own constants too.
The header only provides names - it doesn’t change what any function does. A channel that isn’t writeable still refuses Runtime::Write whether it’s addressed by RT_ name or by number.
Info
#define is used in preference to const uint declarations because unused defines have no effect on the compiled output. If you create 100 variable declarations (const or not), you will add 100 variables to the compiled script binary, that must also take up space on the stack when running.
String/Debug Output
Print, PrintLine, PrintInt, PrintFloat, PrintFormat and PrintBuffer all write to the same destination: a debug message sent to a connected PC over the device’s Ethernet port, rate-limited so a busy loop can’t flood it. None of them do anything unless all of the following are true:
- MectriCal is connected to the device and has debug output active, AND
- The specific script has its “Debug” option enabled in its configuration.
[IMPORTANT] Always disable debug when it’s no longer required.
str in Print/PrintLine/PrintFormat must be a string - a compile-time string constant, not a byte[] buffer. Use PrintBuffer for buffer content instead (see String Buffers).
System::Print
Prints a string literal with no newline.
System::PrintLine
Prints a string literal followed by a newline. Use it for the last piece of a line built up from several Print/PrintInt calls. An empty string "" is not accepted as an argument, so end the line with real text, or use PrintFormat with a \n in its format string.
System::PrintInt
Prints i as a single integer value.
System::PrintFloat
Prints f as a decimal float (%f format).
System::PrintFormat
Prints str as a printf-style format string with f as its one and only argument.so only a single %f-style specifier makes sense. Anything else in the string that consumes an argument is undefined, the same way a mismatched printf format is in C.
System::PrintBuffer
Prints buf’s contents up to its null terminator, or up to capacity bytes, whichever comes first.
See StringBuffers.
String Buffers
string constants are immutable and can’t be built or modified at runtime - for text a script assembles itself (formatting a value, building a label from CAN data, etc.) it needs a byte[] buffer instead, plus the five string natives that operate on it. All five are C snprintf-style: every one takes an explicit capacity and truncates rather than overflowing.
Two easy mistakes to make:
StrCopy/StrAppend’s source (src) is always astringconstant, never anotherbyte[]buffer. There’s no buffer-to-buffer append.- A
byte[]buffer can never be passed where astringparameter is expected. A freshly declared buffer is zero-filled, soStrLengthon one that’s never been written returns0.
System::StrLength
Scans up to capacity bytes for a \0 and returns how many bytes precede it: 0 if buf starts with \0, capacity if no terminator is found.
System::StrCopy
Copies src into dest, truncating and null-terminating to fit within destCapacity.
System::StrAppend
Appends src onto dest’s existing null-terminated content, truncating and null-terminating to fit within destCapacity.
System::IntToStr
Formats value as decimal text into dest, truncating and null-terminating to fit within destCapacity.
System::StrEquals
Returns true if a and b’s null-terminated contents are identical, each scanned up to its own capacity.
Timing
NowMs/NowUs read the device’s uptime from the same free-running hardware timer, but with different wrap periods.
Tip
Write comparisons the wraparound-safe way (unsigned subtraction) rather than assuming now only ever increases:
(uint)(now - start) >= threshold)
System::NowMs
Uptime in milliseconds. Wraps every ~49.7 days (2^32 milliseconds), which makes it the safer default for a script tracking longer-running state (e.g. time since ignition-on).
System::NowUs
Uptime in microseconds. Wraps every ~71.58 minutes (2^32 microseconds).
Because the two clocks wrap at different points, NowMs() and NowUs() / 1000 only agree with each other for the first ~71.58 minutes after boot - once NowUs has wrapped, NowMs keeps counting while NowUs has reset near zero, so don’t rely on them staying numerically related.
System::Yield
Pauses the calling script for t milliseconds, letting other threads (and the rest of the script scheduler) run. This is the standard way to pace a script’s own loop instead of spinning:
System::YieldUntil
A fixed-period yield: sleeps until lastTime + delay, then returns the new lastTime (lastTime + delay) for the caller to feed back in on the next iteration. Unlike Yield, which always sleeps for a fixed duration from now, this keeps a loop’s period accurate even if the loop body’s own work takes a variable amount of time each iteration:
The function takes and returns lastTime explicitly rather than updating it in place (MecScript has no reference parameters) - always reassign the return value back onto the variable you pass in, or the period will drift.
Runtime Channels
Runtime channels are the live values used throughout the TCM (engine RPM, gear, clutch pressures, launch targets and so on). Each is identified by its Runtime ID, available by name as RT_... from the device’s include file.
Tip
See the Runtimes reference document for a list of all runtime channels, their #define names, factors, decimals etc.
Channels come in two flavours from a script’s point of view:
Read/Writework in the channel’s integer representation: the real value with its decimal point removed, so a channel displayed as123.4(one decimal place) reads as1234, and writing1234sets it to123.4. A channel with no decimal places reads and writes as-is.ReadReal/WriteRealwork in floating point units (123.4) and are the simpler choice unless you specifically want integer maths. The TCM is equipped with an FPU so floating point operations are trivial.
Reads of an ID that doesn’t resolve to anything return 0. Writes only succeed (return true) if the target channel is writeable. Lots of channels are computed outputs, not inputs, and will refuse the write.
Important
Channels driven by Input Functions such as Engine Speed must have their input assignment set to Script or they cannot be written to.
Runtime::Read
Reads the channel as an integer with the decimal point removed. Returns 0 if id is unknown.
Runtime::ReadReal
Reads the channel as a float in its real units. Returns 0 if id is unknown.
Runtime::Write
Writes value, an integer with the decimal point removed, to the channel. Returns true if the channel accepted the write, false if id is unknown or the channel is not writeable.
Runtime::WriteReal
Writes value, a float in the channel’s real units.
Returns true if the channel accepted the write, false if id is unknown or the channel is not writeable.
CAN Bus
Every CAN:: function takes a node as its first argument (0 indexed):
0= CAN 1,1= CAN 2Subscribe/Unsubscribealso accept2= both buses.
Node Setup
CAN::SetupNode
Configures and restarts a CAN node from the script, replacing the node’s settings from the calibration.
Returns true on success, false if node is not 0 or 1.
enable-falseturns the node off entirely (no transmit, no receive, termination off). The other arguments are still stored but have no effect until the node is enabled again.bitrateSelect0= Custom bit timing1= 125 kbit/s2= 250 kbit/s3= 500 kbit/s4= 1 Mbit/s- *Any other value leaves the bit timing as it was.
termination- switches the node’s on-board 120R termination resistor on or off.listenOnly-trueputs the node in silent mode: it receives frames but never transmits or acknowledges, and the node’s configured CAN transmit channels are stopped. Use it to read from a bus the TCM must not disturb.
Calling it restarts the node, so any frames in flight on that bus are lost. Call it once at script start, NOT from inside the loop. The new settings are not saved to flash by the call itself, so a script that depends on them should apply them every time it starts.
Raw CAN I/O (polling)
Send/Read are the simple, polling pair. Both target one bus, so node must be 0 or 1. 2 is rejected (Send does nothing, Read returns -1).
CAN::Send
Queues a CAN message with identifier id and the first length bytes of buffer (at most 8) for transmission on node. The call returns as soon as the frame is queued; it does nothing if node is not 0 or 1 or length is 0.
CAN::Read
Copies the most recently received frame with identifier id on node into buffer, up to length bytes, and returns the number of bytes copied. Returns -1 if no frame with that id has been seen on that bus yet.
The device keeps the latest frame for each node/id pair a script has asked about, and each Read call copies out of that store, so reading a frame doesn’t consume it: the same frame is returned again until a newer one arrives. The system holds a fixed number of node/id pairs (16, shared across all running scripts); once it’s full, Read on a pair it isn’t already tracking returns -1. This is fine for a handful of ids a script cares about. If you need every frame on a range of ids, or lower latency, use the callback API below instead.
CAN Receive Callbacks
CAN::Subscribe registers a script function to be called for every received frame whose id matches an id/mask pair, on CAN 1, CAN 2 or both. One subscription can cover many ids (see Id/Mask Matching). Handlers don’t run the instant a frame arrives: matching frames are queued for the script, and CAN::Poll() - called from the script’s own loop - runs each handler in turn on the script’s own thread. This means a callback can never delay real CAN traffic, but also means it only ever fires when the script calls CAN::Poll().
CAN::Subscribe
Subscribes a handler to a range of CAN messages:
node-0= CAN 1,1= CAN 2,2= both (CAN1,CAN2,CAN_BOTHfrom the include file).idMatch/idMask- a frame matches when(frame.id & idMask) == (idMatch & idMask). A mask of0matches every id on the node.handler- a bare script function name (no parentheses, no namespace), which must be declared with exactly this signature:
id is the received frame’s identifier, data holds its payload and length is how many of those bytes are valid (0-8). A handler with the wrong parameter count or types is rejected at compile time.
data is only valid until the handler returns. Index it, or pass it straight on to another function that takes a byte array (such as CAN::Send), but copy it into a script-declared array if you need the bytes afterwards. Each frame delivered to a handler also uses a little extra script stack on top of the handler’s own locals, so a script running very close to its stack limit can halt with a stack-overflow fault on delivery rather than silently missing the callback.
Calling Subscribe again with the same node/idMatch/idMask replaces the existing subscription’s handler rather than adding a second one.
CAN::Unsubscribe
Removes a subscription. Pass the exact same three values it was registered with.
CAN::Poll
Runs the handlers for every matching frame received since the last call, oldest first, and returns how many handlers were fired. A subscribing script must call it once per loop iteration, before yielding, or queued frames simply sit until the next call.
The queue holds a fixed number of frames per script. If a script falls behind a busy bus (e.g. it’s blocked doing other work), the oldest undelivered frames are dropped rather than the queue growing. Keep the loop tight and call Poll() regularly if you’re subscribing to high frequency message(s) or a wide mask. Calling Poll() from inside a handler (directly or indirectly) does nothing and returns 0.
Id/Mask Matching
The match is a bitwise AND against both sides, not a range check:
- Bits set in
idMaskare the ones that must match. - Bits clear in
idMaskare don’t-care and match any value.
A few common patterns:
The frame’s actual ID is always passed to the handler as its first argument, so a single wide-mask subscription can still tell which specific id triggered each call:
Output Control
Script outputs are a dedicated pool of up to 24 output channels that can be used to drive solenoids etc.outputId ranges from 1-24, not 0-based. ID 0 or anything above 24 resolves to nothing and does nothing.
Each output has to be assigned to a physical pin in the device’s configuration before a script can usefully drive it. An unassigned output accepts every call here without error but has no effect on hardware.
Info
The script has no way of knowing which physical output the user assigned to a given Script Output.
Eg: Script Output 1, with ID 1, maybe be assigned to drive Solenoid Output 7 (or any other output pin by the end user).
Info
SetDuty and SetCurrent set the output’s drive mode. Calling one switches the output into that mode and zeroes the other’s setpoint, so the two are mutually exclusive per output.
Whichever was called most recently wins.SetFreq applies to both modes.
Output::SetActiveLevel
Sets whether the output is driven active-high (1) or active-low (0).
LOW and HIGH definitions are in the device include file.
Output::SetEffectiveResistance
Auxiliary Outputs Only
Tells the current control system the resistance of the connected load. Used to help translate the current target into a drive duty. Only relevant in current-control mode when the output is assigned to an auxiliary output rather than a solenoid output.
Info
If the output’s effective resistance is set to 0 (Recommended and default), the system will work it out on it’s own. The only drawback is that the first application of the solenoid may take a few extra milliseconds to settle on the current target.
Tip
Where possible, preference using the solenoid outputs, not auxiliary outputs to drive current-controlled loads.
Output::SetFreq
Sets the output’s switching frequency in Hz:
- PWM rate in PWM (duty) mode.
- Base frequency in current-control mode.
PWM Range: 0.5 - 20000 Hz
Current Control Range: See below
Current Control Frequency
When an output is in current control mode, it’s frequency must be one of the following options:
- 110 Hz
- 200 Hz
- 300 Hz
- 400 Hz
- 500 Hz
- 600 Hz
- 700 Hz
- 800 Hz
- 900 Hz
- 1000 Hz
- 2000 Hz
- 3000 Hz
- 4000 Hz
PWM Mode Frequency
In PWM mode, any frequency up to 20000 Hz (20 KHz) is fine.
Once the duty cycle has been set, putting the output into fixed PWM mode, the frequency can be changed at will, even without updating the duty cycle again. This makes it possible to do variable frequency outputs such as a tacho output.
Output::SetDuty
Puts the output in fixed-duty PWM mode at dutyCycle percent (0-100) and clears any current target.
Output::SetCurrent
Puts the output in closed-loop current-control mode with a target of milliAmps and clears any duty setpoint.
Output Current Range
- Solenoid Outputs in single channel mode can command up to 1.5A.
- Solenoid Outputs in paired channel mode can command up to 2.7A.
- Auxiliary Outputs can command up to 5A. (high side or low side).
Info
Solenoid Outputs are the preferred output for current controlled loads.
Device Identity
A common use is locking a script to a specific device or vendor: compare the serial number and/or a vendor key that only the vendor knows and gives to the user, against an expected value at script start and refuse to run if it doesn’t match.
Device::ReadSerialNumber
Returns the device’s serial number.
Device::ReadVendorKey
Returns one of two general-purpose 32-bit calibration values (“Script Vendor Hardware Key 1/2”), set like any other calibration value in MectriCal. keyId is 1 or 2; any other value returns 0.
Info
The Script Vendor Hardware Key # variables are kept at the device level. This means that uploading a cal file with different values in the keys, will not change the values in the device. They will be ignored from the cal file data. This stops the sharing of cal files that run the same scripts from breaking script execution when uploaded.