Skip to content

MOGWAI NANO v0.3.0

Latest

Choose a tag to compare

@Sydney680928 Sydney680928 released this 28 Aug 16:26
· 1 commit to main since this release

Added

  • makeData — creates a MOGData of a given size, filled with a given byte value (e.g. 1024 0 makeData for 1024 zero bytes, 1023 0xFF makeData for 1023 bytes all set to 0xFF), without pushing each byte onto the stack first. Building a large buffer via repeat { 0 } ->data allocates one MOGObject per byte before conversion, which can exhaust memory on lower-RAM devices even though it works fine on more capable ones; makeData avoids that transient peak entirely

  • nano.send — sends an arbitrary string to the connected device: "TIME=15:45" nano.send. The device fires a STUDIO_DID_SEND event on the currently running program, with the received string available as eventData (a plain MOGString), letting a long-running program (forever do { ... }) react to free-form commands from Studio without needing to be stopped and relaunched. No built-in message format is imposed — parsing the string (e.g. a NAME=value convention) is entirely up to the receiving script

  • ->vars, ->safeVars, ->params — ported with 100% functional parity from the desktop MOGWAI engine. ->vars extracts values from a record or the stack straight into matching local variables, with no type checking beyond raising an error if the stack doesn't have enough elements. ->safeVars does the same but also validates each value's type against a declared record, and is what to ... with [...] do uses automatically for typed function parameters. ->params validates a named-parameter record (with optional default values) against a declared shape, raising an error if a required parameter is missing or mistyped, and silently ignoring extras

  • Bitwise operators on .number& (AND), | (OR), ^ (XOR), ~ (invert), <</>> (shift left/right). Note the context-sensitive parsing of &: &A right before a name is still the existing reference sigil, while X Y & after two numbers already on the stack is the bitwise AND primitive — the two are distinguished by position, not a separate symbol. No .binary/B: type was introduced for this — these operators work directly on regular numbers, which was enough for the intended use cases (e.g. manipulating individual pixels in a display frame buffer) without the overhead of a whole new type family

  • floor, mod — standard floor and modulo, useful alongside the new bitwise operators for address/offset arithmetic (e.g. computing a byte offset and bit position from pixel coordinates)

  • Engine.Idle() — a cooperative yield point called from MOGCode.Execute() on every executed item (both inside and outside loops), calling Thread.Sleep(0) every 10 iterations. The nanoCLR schedules threads cooperatively rather than preemptively (see the nanoFramework thread execution docs) — a tight loop that never yields can starve other threads of CPU time, including the device's own network thread. This was observed concretely: two empty nested loops running for more than ~10 seconds would prevent the TCP thread from ever getting a chance to run, eventually causing MOGWAI NANO Studio to lose the connection even though the device itself was working correctly the whole time

  • SSD1306 OLED display support — a dedicated, native (non-RPN) primitive family wrapping the nanoFramework.Iot.Device.Ssd13xx binding, added after measuring that dense per-pixel drawing in pure RPN (deep-cloning/re-parsing overhead per function call) was multiple orders of magnitude too slow for practical use. Fixed to 128x64 resolution over I2C Fast Mode for now — MOGWAI NANO officially supports this specific display type rather than exposing a generic OLED abstraction. Devices are managed as a single global instance (no name-based multi-display support yet), covering: ssd1306.init (bus, address), ssd1306.close, ssd1306.clear, ssd1306.printString/ssd1306.drawString (x, y, text, size, center) — printString uses character-grid coordinates (like a text console, x=0 y=1 meaning the start of the second text line), while drawString uses pixel coordinates for precise, free-form placement, ssd1306.refresh, ssd1306.drawPixel, ssd1306.drawHorizontalLine/ssd1306.drawVerticalLine, ssd1306.drawRectangle (outline, hand-composed from four calls to the horizontal/vertical line primitives since the underlying binding has no dedicated rectangle-outline method), ssd1306.drawFilledRectangle, and ssd1306.drawBitmap (drawing a raw MOGData buffer as a 1-bit-per-pixel image). A dedicated MW.52x error range covers display-specific failures (already open, not open, initialization failure, general operation failure)

  • The device now declares 'SSD1306' as an additional skill alongside 'GPIO' and 'I2C', queryable the same way ('SSD1306' hasSkill)

  • The new ssd1306.* primitives are recognized and syntax-highlighted by the MOGWAI VS Code extension, following the same zero-modification stub pattern as the other NANO-specific primitives

  • Lazy parsing / frugalMode — a major memory management addition, refined over the course of extensive testing into a clean, unified mechanism. Previously, running a script parsed its entire source into a full object tree upfront, and every executed block was deep-cloned (recursively) before evaluation — necessary to protect against in-place mutation (e.g. a list built with +/AddItem inside a loop must start fresh on every iteration, or 3 { (1 2 3) 4 + ? } REPEAT would print a list that keeps growing across iterations instead of the same (1 2 3 4) three times). On a device with only ~54KB of RAM, this meant a script with several function definitions could consume tens of kilobytes just to parse and clone, well before running any real logic — a script of only ~3KB of source text was measured consuming over 23KB during parsing alone.

    MOGCode/MOGFunction now support two modes, toggled with true/false mogwai.frugalMode, switching takes effect on the very next execution of any block regardless of which mode it was originally parsed under:

    • cool (the default) — parse a block once on first use, keep the resulting object tree cached, and protect against mutation between repeated executions (loop iterations, repeated calls) via deep-cloning. Fast, but memory cost is proportional to total program size and never recovered until the block itself is discarded.
    • frugal — parse a block lazily on first use, discard the parsed objects immediately after each execution, and protect against mutation by simply re-parsing from source on the next call rather than cloning — the untouched source text guarantees a fresh, uncorrupted object tree every time. Flat, stable memory footprint regardless of program size or loop iteration count, at the cost of repeating the parsing work on every single call.

    Measured on a real 128x64 OLED display test (nested loops drawing 121 pixels, each via a function call): 11s in cool mode vs 23s in frugal mode, in a Release build — roughly a 2x speed trade-off for a flat memory profile. (Note: this gap nearly disappears in a Debug build, where cloning and re-parsing end up costing about the same — always benchmark this trade-off in Release.) Cool mode combined with lazy parsing (parsing only what's actually invoked, rather than upfront) already recovers most of the original memory problem for typical scripts; frugal mode remains the right choice for very long-running programs with many repeated calls on memory-constrained devices.

Updated

  • mogwai.info (device-side) and nano.info (Studio-side) records now also include a skills: key, listing the same skills queryable via skills/hasSkill — lets you check a device's capabilities from a single info call, without a separate query
  • RPN stack storage: replaced the ArrayList-backed execution stack with a dedicated MOGStack class using a plain MOGObject[] array with manual growth (doubling capacity as needed, starting small to keep the per-scope memory footprint low). Since every value on the stack is already a MOGObject reference, this removes ArrayList's generic overhead entirely with no boxing trade-off — measured as a very significant speedup on stack-heavy operations (e.g. building a large MOGData buffer by pushing hundreds of values with repeat before converting with ->data)
  • I2C write primitives confirmed working with large multi-byte buffers in a single transaction (not just single bytes or short sequences) — validated by initializing and clearing a full 128x64 OLED display (SSD1306) frame buffer (1024 bytes) in one i2c.register.write call
  • I2C write primitives now accept a MOGData buffer by reference (&myBuffer) rather than only by value, avoiding an unnecessary copy of the buffer on every call — most useful for a large, frequently-updated buffer like a display frame buffer
  • FOR/FORSTEP no longer allocate a new MOGNumber for the loop counter on every iteration — the same object is now reused and its value updated in place. This changes loop variable semantics slightly: a reference to the loop variable (&i) always reflects its current value, even after the loop has moved on — code that needs to preserve a snapshot of the value from a specific iteration (e.g. collecting values into a list) must explicitly copy it (i -> 'snapshot') rather than storing a reference to the loop variable itself
  • The outgoing message queue is now split into a priority lane (PROGRAM.DID.START, PROGRAM.DID.STOP, STATE.GET, PONG, and other control messages) and the regular capped lane (console.print/debug.write output). The priority lane is never capped and is always drained first, so a program producing a lot of console output can no longer delay or push out a control message that MOGWAI NANO Studio is actively waiting on

Fixed

  • If MOGWAI NANO Studio was killed abruptly while a program on the device kept sending console/debug output, a new connection attempt would silently hang for up to 30 seconds (the idle timeout) before succeeding — the failed writes on the old, dead connection were detected but never actually signaled anywhere, leaving the device's TCP accept loop stuck on the stale connection. A shared flag now lets a failed write immediately unblock the read loop, so a fresh reconnection succeeds right away instead of waiting out the timeout
  • Three more occurrences of the static-initialization-order issue already described for EvalResult.NoError/the Error class (see below) were found and fixed: EvalResult.Error itself (via a C# 9 init accessor combined with a field initializer — replaced with a plain constructor-assigned property), and MogwaiNanoEngine.LastResult/LastError (both { get; set; } = ... property initializers referencing another class's static member — replaced with the same lazy-initialization pattern). Any of these being null at the wrong moment could crash the device's execution dispatcher with a hard-to-diagnose CLR_E_WRONG_TYPE/NullReferenceException pair
  • Error.FatalError was declared but never actually registered in Error's lazy initialization — a missing line meant it silently stayed null forever. Since it's used by MOGCode's catch-all safety net for unexpected exceptions during script execution, this masked the real underlying error message every time that safety net triggered, itself crashing on the same null-related symptom instead of reporting what actually went wrong
  • Fixed the operand order in mod: y 8 mod was computing 8 mod y instead of y mod 8, which happened to go unnoticed on small test values before producing an out-of-range result on a real calculation (30 mod 8 returning 8, an impossible modulo-8 result)
  • MOGData.Clone() didn't actually copy its underlying byte array — it shared the same array reference with the original, so mutating one through set would silently corrupt the other. Now copies the array on clone
  • I2C register writes were briefly split into two separate WriteByte/Write calls (as a memory optimization, to avoid allocating a combined buffer on every call) — this broke the repeated-start requirement some I2C devices rely on to treat the register address and the following data as a single logical write, causing an OLED display (SSD1306) to silently accept every command over I2C without ever actually updating its output. Reverted to a single combined write; lazy parsing/frugalMode (see above) turned out to be the right way to address the original memory concern instead
  • FOR was missing a return on its early argument-count failure path — if the stack didn't have enough elements, it constructed an error result but then fell straight through to the type-check line anyway, indexing into an already-empty array and risking a lower-level crash instead of cleanly reporting the error
  • MOGWAI NANO Studio was treating an ordinary TCP read timeout (used to periodically re-check the connection, no message received in the configured window) as a hard disconnection — reported as Unable to read data from the transport connection. A program that stays quiet for a while (heavy computation with no console output in between) would trip this every time the timeout elapsed, even though the connection and the device were both fine. A plain read timeout is no longer treated as a disconnect

Known Limitations

  • Occasional network disconnections between MOGWAI NANO Studio and a device are expected over WiFi — they can originate on either side (device WiFi hiccups, or a transient network interruption on the PC) and the exact interval is not constant. Reconnection is fast and doesn't require restarting Studio.