Scriptable control panels for lab instruments over serial
Rushabh’s instruments talk over a serial port in fixed-width strings. A wrong command fails quietly. The first control app was the usual Android approach: hardcode a screen per machine, wire each button to a string, ship a build. That worked until the next panel showed up. Then it was another layout, another activity, another release for what was often “just one more button.” Technicians needed changes faster than Studio releases allowed, and behaviour had to depend on what the instrument sent back, not only on what the app sent out.
Technologies
Android (Java) · custom DSL / lexer / AST executor · expression evaluation · serial port I/O (android_serialport_api) · Sora editor for script editing · SharedPreferences / local script storage · Gradle product flavors (dev/prod)
01
Problem
What had to be solved.
Rushabh’s instruments talk over a serial port in fixed-width strings. A wrong command fails quietly. The first control app was the usual Android approach: hardcode a screen per machine, wire each button to a string, ship a build. That worked until the next panel showed up. Then it was another layout, another activity, another release for what was often “just one more button.” Technicians needed changes faster than Studio releases allowed, and behaviour had to depend on what the instrument sent back, not only on what the app sent out.
02
My responsibility
What I owned.
I owned the Android app end to end: serial I/O, the scripting language (lexer, statement model, expression evaluator, runtime), the panel UI (photograph plus placed buttons and labels), multi-screen navigation, and the rewrite from an interpret-as-you-go parser into a parse-then-execute compiler. I also wrote the command docs so people who weren’t Android developers could author scripts.
03
Hard part
Where it got difficult.
Two things collided. First, nested control flow. repeat wrapping if wrapping command needs a real tree and a walker; v1 mixed “parse this line” with “run this line,” so there was nowhere for a stack to live. Second, the protocol itself. Position in the reply is meaning. Scripts needed to branch on slices of the last response and dig through recent replies when something went wrong, without turning technicians into Java programmers. Getting compile errors with line numbers right mattered more than a fancy type system.
04
What I built
The work that shipped.
A small DSL that describes a control panel instead of an Android view hierarchy. A script sets canvas size, orientation, and a photo of the instrument; btn_ / label_ blocks place controls at x/y on that image; actions blocks send serial commands and run logic. Under the hood, script_compiler_v2 tokenizes into typed statements (If, Repeat, Command, CallFunc, RouteTo, and so on), builds nested bodies, and executes the tree. An expression evaluator handles int/float/string inference, arithmetic, comparisons, && / ||, parentheses, and string helpers (left, right, mid, length). Hardware-specific pieces: lastResponse (full string or 1-based inclusive slice) and search(n) for recent reply forensics. Screen lifecycle via func_init / func_dispose around routeTo. Global $ variables in a singleton registry. Serial transport through the Android serial port API with configurable baud. Roughly 8,800 lines of Java across the pieces that matter; the evaluator alone is ~800 lines. v1 stays in the tree as the version that taught the split.
05
Result
What changed.
A new panel is a photo plus a text file, not a Studio release. Technicians can add buttons, wire commands, branch on replies, move between screens, and diagnose faults from response history without waiting on me. The app stopped being “the control UI for one instrument” and became a way to ship panels as data. Tradeoffs I still own: globals that persist across scripts, parameterless callFunc, whitespace that is cosmetic only (blocks close with explicit endif / endrepeat / endfunc). Those were right for the audience; they’d be different choices in a v3.