Proxy mode deep dive: the QA runs on your laptop, the device lives on the HC3

Way back in the deploy post, proxy mode got a teaser it never cashed in. Today's the day. Proxy mode runs your QA in the emulator while a mirror device — <name>_Proxy, same type, same UI — runs on the real HC3. The controller treats it as one device: actions and UI events aimed at the proxy land in the emulator, and every state change the QA makes is pushed to the controller. The phone app shows a real, live device. Your code never left the laptop.

One directive switches it on. Here is the whole demo — a QA with a button, a counter in its internal storage, and a property update:

--%%name:proxy-demo
--%%mode:proxy
--%%u:{button="B1",text="Tap me",onReleased="tap"}
-- --------------- EOH ---------------
function QuickApp:onInit()
  print("proxy id:", self.id)   -- the proxy's HC3 id, not an emulated one
  local visits = tonumber(self:internalStorageGet("visits") or "0") + 1
  self:internalStorageSet("visits", tostring(visits))
  self:updateProperty("value", true)
  print("visit:", visits)
end
function QuickApp:tap()
  print("TAPPED")
end

Run it online — flua deploys the proxy and the QA boots under the proxy's HC3 id:

flua: proxy installed: 4387 proxy-demo_Proxy
🚀 flua 0.1.126 (Lua 5.4, Python 3.14.3), mode proxy:4387
[proxy-demo4387]: proxy id: 4387
[proxy-demo4387]: visit: 1
flua: proxy connected — proxy-demo_Proxy (4387) sends actions and UI
events to http://192.168.50.129:8081

Note self.id: it is 4387, the controller's id — not an emulated 5000-something. The emulated QA and the HC3 device are the same id. And the QA never terminates: a proxy QA keeps flua alive, because the callbacks must have somewhere to land.

The state lives on the controller

Ask the HC3 what it knows about the device — the property update and the internal storage write went straight to the controller:

GET /devices/4387            -> 200, name: proxy-demo_Proxy, value: true
GET /plugins/4387/variables  -> [{name:"con", value:{ip:"192.168.50.129", port:8081}},
                                 {name:"visits", value:"1"}]

Now the money shot. Kill flua. Start it again. The proxy is reused, the QA boots, and the counter reads its previous value — from the HC3, not from any local state:

[proxy-demo4387]: proxy id: 4387
[proxy-demo4387]: visit: 2

That's the proxy mode persistence story from the last post made concrete: a proxy QA's variables — and its children's — live on the controller. Reads answer from the HC3 (through the helper bridge), writes land there first and the emulator mirrors after. Kill and restart flua all you like; the state was never on the laptop. Libraries that stash their state in child variables survive too.

Tap the button

Open the phone app, find proxy-demo_Proxy, tap. The HC3 delivers the UI event to the emulator's callback endpoint, and your QA runs the handler exactly like a tap in the simulated UI:

[proxy-demo4387]: TAPPED

Same for actions and sliders, and same for children: a createChildDevice creates the child on the HC3, an existing proxy's children are shadowed under their HC3 ids at startup, and their events route back like the parent's.

The fine print

Next time: endpoint packages — wrap a simulated endpoint in a QA and distribute it as a .fqa, emulation like any other QA.