What device platform engineering is, the parts of a managed device, what Poly's public documentation says about its video systems and PolyOS, how one admin click travels from the web UI to the operating system, how an OS migration like 4.x → 5.x is run, and how to talk about all of it in the interview.
6 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.
Device platform engineering is building the software that lets a fleet of hardware be set up, controlled, monitored and updated: the on-device web UI an admin opens in a browser, the REST and command-line APIs that integrators script against, the agent that talks to cloud management, and the glue between all of that and the operating system underneath. It's full stack work, but the "server" is a video bar bolted above a TV in a meeting room, and there are tens of thousands of them, in buildings you'll never visit.
A good mental model: a web app is a restaurant; a device fleet is a meal-kit company. In a restaurant everyone eats in your building, and when you change the recipe every customer gets the new dish at the next meal. A meal-kit company ships boxes to thousands of kitchens it never sees. Each kitchen cooks whatever version of the kit it last received, with its own oven, its own quirks, and sometimes a box that has been sitting in the cupboard for months. You can't walk in and fix a dish; you can only send a better box and hope it gets opened.
That changes the engineering. On a web service there is one version in production (or two, during a deploy), rollback is a button, and logs are a query away. On a device fleet many versions are live at once for years, some updates are one-way, the device may be offline or behind a firewall when you need it, and other companies sit in your release path. HP says Poly VideoOS 5.0 "establishes the foundation for future updates to Android 17 and support through 2032" (HP newsroom, 3 February 2026): a release shipped in 2026 is planned to be supported for six more years.
Web service
Device fleet
Who runs the code
your servers
thousands of devices on customers' networks
Versions in production
one, two during a deploy
many, for years
How an update arrives
you deploy it
the device pulls it, an admin pushes it, or a partner's console delivers it
Rollback
redeploy the old build
sometimes impossible: Poly VideoOS 5.0 is a one-way upgrade
Debugging access
logs, traces, a shell
whatever the device exposes: log bundles, diagnostics pages, a remote session if allowed
Who else is in the release path
usually nobody
conferencing partners, IT departments, maintenance windows
What "down" means
an error rate on a dashboard
a meeting room that can't start the CEO's call
Those rows come from real public facts about the platform this role works on:
Updates are not always automatic. Poly's configuration guide lists upgrade.auto.enable with the default "False (Teams and Zoom default)" and "True (All other conferencing providers default)" (Poly VideoOS Configuration Parameter Reference Guide 4.1.0, HP, PDF dated November 2023). A Teams or Zoom room waits for an admin.
Partners deliver firmware. Microsoft says Android OS and firmware updates for Teams devices "are provided by hardware Original Equipment Manufacturers (OEM) to Microsoft for certification" and then distributed through the Teams Admin Center (Microsoft Learn, "Android versioning support for Teams devices", last updated 28 September 2026). The HP Support Community's post for 5.0.0 says the release "will be delivered initially via the Microsoft Teams Admin Center" (community knowledge-base post "PolyOS 5.0.0", 6 May 2026).
The model below shows what that means for the versions in the field. A thousand devices get a new release, 4.6. Forty percent update themselves through a staged rollout; the rest wait for an admin, as Teams and Zoom rooms do by default. Five percent are unreachable in any given week.
fleet-versions.mjs
// A fleet of 1,000 room devices gets a new release (4.6). Who is on what, week by week?// Seeded PRNG (mulberry32) so the output is the same on every run.functionmulberry32(seed){return()=>{
seed |=0; seed =(seed +0x6d2b79f5)|0;let t = Math.imul(seed ^(seed >>>15),1| seed);
t =(t + Math.imul(t ^(t >>>7),61| t))^ t;return((t ^(t >>>14))>>>0)/4294967296;};}const rand =mulberry32(42);// Starting mix of versions, and how each device gets updates.const fleet = Array.from({length:1000},(_, id)=>{const r =rand();return{
id,version: r <0.5?"4.5": r <0.8?"4.4":"4.3",autoUpdate:rand()<0.4,// the rest wait for an admin to push the updatering:rand(),// position in the staged rollout: 0..1};});// Staged rollout for auto-updating devices: share of the fleet eligible each week.const eligibleShare =[0,0.01,0.1,0.5,1];constshare=(week)=> eligibleShare[Math.min(week, eligibleShare.length -1)];
console.log("week on 4.6 versions in the field");for(let week =0; week <=12; week++){for(const d of fleet){if(d.version ==="4.6")continue;const online =rand()>0.05;// 5% are switched off or unreachable in any given weekif(!online)continue;if(d.autoUpdate && d.ring <share(week)&&rand()<0.9) d.version ="4.6";elseif(!d.autoUpdate && week >=2&&rand()<0.12) d.version ="4.6";}if(week %2===0|| week ===1){const counts ={};for(const d of fleet) counts[d.version]=(counts[d.version]??0)+1;const mix = Object.keys(counts).sort().map((v)=>`${v}:${counts[v]}`).join(" ");
console.log(`${String(week).padStart(4)}${String((counts["4.6"]??0)/10).padStart(5)}% ${mix}`);}}const old = fleet.filter((d)=> d.version !=="4.6");
console.log(`after 12 weeks: ${old.length} devices still on an older version (${old.filter((d)=> d.version ==="4.3").length} on 4.3)`);
Result
week on 4.6 versions in the field
0 0% 4.3:188 4.4:289 4.5:523
1 0.2% 4.3:188 4.4:289 4.5:521 4.6:2
2 9.2% 4.3:172 4.4:269 4.5:467 4.6:92
4 53.2% 4.3:88 4.4:139 4.5:241 4.6:532
6 66.2% 4.3:69 4.4:98 4.5:171 4.6:662
8 72.8% 4.3:49 4.4:77 4.5:146 4.6:728
10 77.3% 4.3:41 4.4:71 4.5:115 4.6:773
12 82% 4.3:36 4.4:53 4.5:91 4.6:820
after 12 weeks: 180 devices still on an older version (36 on 4.3)
What’s happening
The fleet starts on three versions (4.3, 4.4, 4.5: 188, 289 and 523 devices). That's normal: nobody's fleet is ever on one version, because each earlier release reached it in the same uneven way this one will.
Each device gets two random properties: autoUpdate (about 40% true) and a ring number between 0 and 1. The ring is how a staged rollout works: in week 1 only devices with ring < 0.01 are eligible, in week 2 ring < 0.1, then half, then everyone. Week 1 shows the canary: just 2 devices.
Every week, a device that's offline (5%) is skipped; an eligible auto-updating device updates with 90% probability (the maintenance window might not come round); a manual device updates with 12% probability from week 2, standing in for admins working through their rooms. The PRNG is seeded, so every run prints the same table.
Adoption shoots up while the rollout widens (9.2% → 53.2% between weeks 2 and 4) and then flattens: 66% at week 6, 82% at week 12. The fast part is the auto-updaters; the long tail is the manual rooms and the devices that are switched off.
After twelve weeks, 180 devices are still on an older version, 36 of them two releases back on 4.3. Anything this release added to the API, the web UI or the CLI is missing on 18% of the fleet. That's the core lesson: a device API is used by many versions at once, so every change has to work across all of them (see Rules for compatible change).
If you changed the manual update chance from 12% to 0 (rooms whose admins never update), the tail would never close. Real fleets have those rooms, which is why vendors set support windows and why a partner's certification end date (Microsoft's, for Teams devices) can force an OS migration.
AdvancedWhat this means for the work
Every API is a published API. Integrators script against it, partners build on it, and old scripts keep running against new firmware. You can add; you can rarely remove or rename (see Why devices make versioning hard).
Clients must discover, not assume. A web UI served by the device only has to match that device's firmware, but cloud management, SDKs and partner integrations talk to many versions and must ask what each device supports (Capability discovery).
Diagnostics are a feature. You can't SSH into a customer's meeting room at will, so log bundles, status pages and health telemetry have to be designed in. Poly's REST API, for example, has GET /rest/system/logs, which returns "the system logs in a .tar file" (Poly VideoOS REST API Reference Guide, HP document P55757-001A, August 2025).
Security is everyone's job. Devices sit on corporate networks for years, often with admin interfaces reachable from the LAN. Defaults, password changes, disabled legacy protocols and signed updates are part of the product (Security for Device APIs).
Partners are part of the system. A room running Microsoft Teams or Zoom Rooms runs the partner's app on your platform; their certification rules and update channels shape your release plan.
AdvancedLab results versus field reality
?Can a partner rely on the new endpoint?
A partner integration team asks: "Release 4.6 adds GET /api/v1/rooms/occupancy. Can our cloud service call it on every customer device from next month?" Using the model above, what's the honest answer, and what should their code do?
Show answer
No. A month after release (around week 4) only about 53% of the modelled fleet has 4.6, and even after twelve weeks 18% doesn't. Their code must check per device and handle "not supported".
What’s happening
The release reaching the first devices isn't the release reaching the fleet. In the model, week 4 shows 4.6:532 out of 1,000: roughly half the partner's calls would hit devices that don't have the endpoint.
On a device without the endpoint, the call returns a 404 (or whatever the old firmware does with an unknown path). If the partner treats that as an outage, they'll page themselves for a version mismatch.
The right design is capability discovery: read the device's version or, better, a capabilities document ("occupancy": true), and only call the endpoint where it exists. Unknown devices fall back to the old behaviour.
The flattening curve (66% → 82% between weeks 6 and 12) also tells them not to plan on "everyone will have it soon". Some rooms update only when someone schedules it; some never do until the vendor's support window forces it.
What to say: "You can call it where the device advertises it. Here's the capability flag, here's what older devices return, and here's our rollout schedule so you can see adoption grow."
Every managed device, whether it's a video bar, a printer, a router or a smart thermostat, has the same handful of parts. Front doors take requests: a web server for the admin UI, a REST API for software, a command line for scripts and room control systems. A security layer decides who's asking and what they may do. A command and settings layer does the actual work, with one set of rules behind every door. A config store holds settings in layers (defaults, local changes, centrally provisioned values). Platform services own the hardware: camera, audio, network, the update engine. A device agent keeps an outbound connection to cloud management and provisioning. Underneath sit the operating system and signed firmware images.
A good mental model: a small office building with one filing cabinet. There's a front desk (web UI), a mail slot (REST) and a phone line (CLI); a security guard checks everyone (auth); one office manager handles every request the same way whichever door it came through (command layer); the filing cabinet holds the current settings, with head office's instructions on top (config store with provisioned values); the workshop in the basement does the physical work (platform services); and a courier visits head office every day for new instructions and parcels (device agent). If each door had its own manager and its own filing cabinet, the building would soon disagree with itself.
The parts of a managed device. Requests come in through three front doors and meet one command layer; the device agent reaches out to management services, so the device works behind a firewall.
Here is what each part does, and the public Poly facts that show it on a real product (all from HP's Poly documentation, cited in the next topic):
Part
Its job
Where it shows up on Poly video systems
Web server and admin UI
Lets an admin configure everything from a browser
"Open a web browser and enter the system's IP address"; port 80 "Redirects all sessions to HTTPS on port 443"
REST API
Programmatic access for integrators
/rest/... paths over HTTPS, JSON bodies, a session from POST /rest/session
Command line
Scripted and room-control access
SSH on port 22, Telnet on port 24 (default) or 23, both off by default; RS-232 on the G7500, whose device.serial.mode defaults to "Control"
Sessions, auth, roles
Who is asking, and are they allowed
admin credentials, a session limit (sec.access.maxSessions, default 50), web and SSH port lockout
Command and settings layer
The same rules whichever door a request uses
not described in public docs: this is the design you'd bring
Config store
Settings in layers
named parameters such as sec.telnet.enable; "Provisioned settings are read-only in the system web interface"
Platform services
Own hardware and OS features
cameras, microphones, Wi-Fi, Device Mode; public docs describe features, not the services' internals
Device agent
Talks outbound to management
registers with Poly Lens or a provisioning server, checks for updates "every time it restarts and at an interval set by the service"
OS and firmware
Everything runs on it
Android based; 5.0.0 moved the second-generation systems "from Android 10 to Android 13" and updated "the system's factory restore partition"
The model below builds the middle of that picture in about sixty lines: one config store with layers, written by a REST API, a command line and a provisioning push, with a platform service reacting to a change. It runs with Express 5 and supertest from the site's sandbox; supertest calls the app without opening a fixed port.
mini-device.mjs
// A managed device in miniature: one config store, written by a REST API, a command line and a provisioning// server, with a platform service reacting to changes. Setting names come from Poly's public parameter guide;// everything else here is a teaching model, not Poly's implementation.import express from"express";import request from"supertest";const schema ={"device.local.deviceName":{type:"string",default:"Room-101"},"sec.telnet.enable":{type:"boolean",default:false},"sec.web.idleSessionTimeout":{type:"enum",values:[1,3,5,10,15,20,30,45,60],default:10},};// The config store: three layers, highest wins. Provisioned values are read-only for local writers.const layers ={provisioned:{},local:{}};const listeners =[];constread=(name)=>{const source = name in layers.provisioned ?"provisioned": name in layers.local ?"local":"default";const value = source ==="default"? schema[name].default : layers[source][name];return{ name, value, source,editable: source !=="provisioned"};};functionwrite(name, value, writer){const def = schema[name];if(!def)return{error:"UNKNOWN_SETTING"};const ok = def.type ==="enum"? def.values.includes(value):typeof value === def.type;if(!ok)return{error:"INVALID_VALUE"};if(writer !=="provisioning"&& name in layers.provisioned)return{error:"READ_ONLY_PROVISIONED"};
layers[writer ==="provisioning"?"provisioned":"local"][name]= value;for(const fn of listeners)fn(name, value);returnread(name);}// A platform service that owns the Telnet listener and reacts to the setting.
listeners.push((name, value)=>{if(name ==="sec.telnet.enable") console.log(` platform: telnet listener ${value ?"started on port 24":"stopped"}`);});// Front door 1: REST.const app =express();
app.use(express.json());
app.get("/api/config/:name",(req, res)=>(schema[req.params.name]? res.json(read(req.params.name)): res.sendStatus(404)));
app.put("/api/config/:name",(req, res)=>{const result =write(req.params.name, req.body.value,"rest");const status ={UNKNOWN_SETTING:404,INVALID_VALUE:422,READ_ONLY_PROVISIONED:409}[result.error]??200;
res.status(status).json(result);});// Front door 2: a line-based command interface over the same store.functioncli(line){const[verb, name, raw]= line.split(" ");if(verb ==="get")return`${name}${read(name).value}`;const value = raw ==="true"?true: raw ==="false"?false:/^\d+$/.test(raw)?Number(raw): raw;const result =write(name, value,"cli");return result.error ?`error: ${result.error}`:`ok ${name}${result.value}`;}constshow=(r)=> console.log(`REST ${r.req.method}${r.req.path} -> ${r.status}${JSON.stringify(r.body)}`);show(awaitrequest(app).get("/api/config/device.local.deviceName"));
console.log(`CLI set device.local.deviceName Board-Room -> ${cli("set device.local.deviceName Board-Room")}`);show(awaitrequest(app).get("/api/config/device.local.deviceName"));
console.log("PROV push sec.web.idleSessionTimeout=30 ->",write("sec.web.idleSessionTimeout",30,"provisioning").source);show(awaitrequest(app).put("/api/config/sec.web.idleSessionTimeout").send({value:10}));
console.log(`CLI set sec.telnet.enable yes -> ${cli("set sec.telnet.enable yes")}`);show(awaitrequest(app).put("/api/config/sec.telnet.enable").send({value:true}));
console.log(`CLI get sec.telnet.enable -> ${cli("get sec.telnet.enable")}`);
Result
REST GET /api/config/device.local.deviceName -> 200 {"name":"device.local.deviceName","value":"Room-101","source":"default","editable":true}
CLI set device.local.deviceName Board-Room -> ok device.local.deviceName Board-Room
REST GET /api/config/device.local.deviceName -> 200 {"name":"device.local.deviceName","value":"Board-Room","source":"local","editable":true}
PROV push sec.web.idleSessionTimeout=30 -> provisioned
REST PUT /api/config/sec.web.idleSessionTimeout -> 409 {"error":"READ_ONLY_PROVISIONED"}
CLI set sec.telnet.enable yes -> error: INVALID_VALUE
platform: telnet listener started on port 24
REST PUT /api/config/sec.telnet.enable -> 200 {"name":"sec.telnet.enable","value":true,"source":"local","editable":true}
CLI get sec.telnet.enable -> sec.telnet.enable true
What’s happening
schema is the single description of every setting: its type, its allowed values and its default. The three names are real Poly parameters from the public configuration guide (sec.web.idleSessionTimeout really allows 1, 3, 5, 10, 15, 20, 30, 45 or 60 minutes with 10 as the default); the store and handlers around them are mine.
The first REST read of device.local.deviceName finds nothing in the provisioned or local layers, so read returns the default "Room-101" with source: "default". Returning the source matters: a UI can show "inherited from defaults" or "managed centrally" next to a field.
The CLI writes Board-Room. It goes through the same write function as REST would, so it's validated by the same schema and lands in the same local layer. The next REST read sees it immediately (source: "local"): two front doors, one truth.
The provisioning server pushes sec.web.idleSessionTimeout = 30 into the provisioned layer. When REST then tries to set it to 10, write refuses with READ_ONLY_PROVISIONED and the REST handler maps that to 409 Conflict. That mirrors Poly's documented behaviour ("Provisioned settings are read-only in the system web interface"): a central policy beats a local edit.
set sec.telnet.enable yes fails with INVALID_VALUE, because "yes" isn't a boolean. The REST PUT with true succeeds, and the listener fires before the response is built: the platform service starts the Telnet listener. That's the line from config to hardware: services subscribe to the settings they own instead of the web layer poking the OS directly.
The final CLI get reads true, written moments earlier through REST. If each door had its own copy of the settings (a common shortcut when a CLI is bolted on later), the CLI would still say false here; the walkthrough in One command layer behind REST, Telnet and SSH builds this properly.
AdvancedLayers, precedence and what survives a reset
The three-layer store in the model is the simplest version of a pattern every managed device has. Real devices usually add more layers and rules:
Factory defaults ship in the firmware image and change only with a new release. A new release can change a default; existing devices keep any value that was set explicitly, so "what is the default?" has to be answered per version.
Local values are set by an admin in the web UI, the REST API or the CLI.
Provisioned values come from a provisioning service and win over local ones. Poly documents that a provisioned system keeps "the most recent configuration it received" if it loses registration with the service, so the provisioned layer is cached, not fetched on every read.
Per-mode applicability. Some settings only mean something in one mode: Poly's parameter guide marks SIP settings with "You can't provision this parameter if the system is in Partner Mode." A settings API needs a way to say "not applicable in this mode" distinct from "invalid".
What survives a reset. Poly's REST API has POST /rest/system/resetsettings with flags keepCertificates, keepCdr, keepDirectory and keepLogs, and its admin guide says "Your system remains registered with Poly Lens even after a reset or factory restore." So "reset" isn't one operation: each part of the store needs a decision about whether it's user data, configuration, identity or history.
?Which layer owns the fix?
On a device like the model's, an admin sets the device name to 300 characters through the CLI. The web UI then renders a broken header, and cloud management rejects the device's heartbeat because the name is too long. Which layer should the fix go in, and why not the others?
Show answer
The schema / settings layer: add maxLength to device.local.deviceName there, so every writer is refused with the same error.
What’s happening
The bad value got in through the CLI, but patching the CLI parser alone leaves REST and provisioning open. The same 300 characters could arrive through PUT /api/config/device.local.deviceName next week.
Fixing the web UI (truncate the header) hides the symptom for one viewer and does nothing for the cloud heartbeat, which also reads the stored value.
In the model, every writer calls write(name, value, writer), which checks schema[name]. Adding a length rule there means CLI, REST and provisioning all get INVALID_VALUE, and the web UI can read the same schema to show the limit before the user presses Save.
One more step a device needs that a web app doesn't: devices already in the field may hold the long value. The new release needs a migration (truncate, or keep and flag it) because the new rule will meet old data on the first boot after the update.
This topic is the product briefing: what HP's public Poly documentation says about the devices, the software they run, the modes they run in and the ways they're managed and scripted. Every fact below is from a public HP, Microsoft or Google source, named with its date; the sources were read on 27 October 2026. Posts in the HP Support Community's Poly knowledge base are cited as community posts: HP hosts them, but each carries HP's disclaimer that it gives "the personal opinions of the authors, not of HP", so they're not official documentation. Where the public documentation doesn't say something, this page says so instead of guessing. Nothing here is internal HP information, and the role's internal project name ("Project Trinity" in the job description) doesn't appear in any public source I could find.
A useful mental model for the product line: one Android-based platform wearing different hats. The same family of room systems can boot into Poly's own video app, into Microsoft Teams Rooms, into Zoom Rooms or another partner's app, or act as a USB camera and speaker for a laptop. The hardware and the platform underneath (settings, web UI, APIs, management, updates) stay Poly's; the conferencing app on top changes with the "provider" the admin chooses. Most of the web UI and API work in this role is that platform layer.
The company and the product lines
HP completed its acquisition of Poly on 29 August 2022 (HP press release, 29 August 2022: an all-cash deal at $40 a share, "implying a total enterprise value of approximately $3.3 billion, inclusive of Poly's net debt"). Poly's documentation has since moved to HP Support, and newer documents and announcements say "HP Poly".
The admin guides (Poly Partner Mode Administrator Guide 4.7.0, HP, "Last update: September 2026", and Poly Video Mode Administrator Guide 4.7.0, HP, "Last update: July 2025"; both PDFs downloaded from HP in October 2026) describe these room systems and accessories:
Family
Products
What the guides say about them
Video bars, first generation
Poly Studio X30, X50, X70
"All-in-one collaboration system", "No need for a separate PC, laptop, or codec"
Video bars, second generation
Poly Studio X32, X52, X72
X72 has "Dual 4K 20MP cameras"; X32 can be powered "with PoE++ (65W)"
Modular systems
Poly G7500, Poly Studio G62
separate cameras and microphones for larger rooms; the G62 has PoE+ power and a link-local network (LLN) port for IP peripherals
Touch controllers
Poly TC8, Poly TC10
paired with a system over the network to control it
Cameras
Poly Studio E60, E70
E60: "12x optical zoom MPTZ camera"; E70: dual-camera USB 4K
USB video bars
Poly Studio V52, V72 (and the V12, which HP announced at ISE 2026; it isn't in these guides)
"Poly Studio V systems don't include a user interface. To change settings, access the system web interface or use Poly Lens."
HP's community post for 5.0.0 calls the X32, X52, X72 and G62 the second-generation systems ("A Gen2 device, aka a Studio G62, Studio X32, Studio X52 or Studio X72"); the official release notes simply list those four models. That split matters for the OS story below.
The software: Poly VideoOS, also called PolyOS
The official name in HP's release notes and guides is Poly VideoOS. The HP Support Community's knowledge-base posts title the same releases "PolyOS" (for example "PolyOS 5.0.0" with the build string PolyOS 5.0.0-418922), and Google's 2023 announcement said "Poly OS 4.0". The role description uses PolyOS; treat the names as the same product line. The touch controllers run a separate OS, Poly TCOS.
It runs on Android. The public evidence:
Poly VideoOS 5.0.0 (release date May 2026) "contains an Android 13 update for only Poly Studio G62, Poly Studio X32, Poly Studio X52, and Poly Studio X72 systems", and "Your system updates from Android 10 to Android 13 and your Poly TC10 touch controller updates from Android 11 to Android 12" (Poly VideoOS Software Release Notes 5.X, HP Support).
5.0.0 "supports only Microsoft Teams and Poly Device Mode. Your device must be in one of these modes to upgrade successfully." The HP Support Community's post for the release lists the providers not supported on 5.0.0: Poly/SIP, Google Meet, RingCentral, Dialpad, GoTo Meeting and Tencent (community knowledge-base post "PolyOS 5.0.0", 6 May 2026; not official documentation).
The upgrade path: "Your system must be running Poly VideoOS 4.6.0 before updating to Poly VideoOS 5.0.0", and a system on an earlier version "automatically loads VideoOS 4.6.0" first. It's one-way: "Downgrading from Poly VideoOS 5.x to Poly VideoOS 4.x versions isn't supported", and the release "also updates the system's factory restore partition", so a factory restore doesn't go back to 4.x either.
"Partner applications are no longer bundled in the main software package. The partner application downloads once you upgrade", from swupdate.lens.poly.com.
The first-generation systems stay on 4.x: "Poly Studio X30, Poly Studio X50, Poly Studio X70, Poly G7500 or Poly TC8 are incompatible with PolyOS 5.0.0" (community knowledge-base post "PolyOS 5.0.0", 6 May 2026; the official notes say only that the four Gen2 models support 5.0.0, without naming the others).
Poly VideoOS 5.0.1 (August 2026) is a maintenance release, still Teams and Device Mode only; one fix it lists is "the device shows as offline in Poly Lens cloud after upgrading" (PVENG-41383).
Poly VideoOS 4.7.0 (June 2026) is a "sustaining release" for "all Poly and partner modes", on both generations. It added a switch to turn off the REST API's XSRF token requirement (more on that below) and "HP Direct Connect" for Teams Rooms touch controllers (community knowledge-base post "PolyOS 4.7.0", 18 June 2026; its fixes cover the G7500 and X30/X50/X70 as well as the Gen2 systems). Its fixes include testing "against both the CVE-2025-48543 Android Runtime Use-After-Free vulnerability and CVE-2024-43093", another sign that 4.x is Android-based.
HP's ISE announcement says Poly VideoOS 5.0 "delivers a major platform upgrade to Android 13", "establishes the foundation for future updates to Android 17 and support through 2032", and "lays the foundation for Poly VideoOS 5.1, including HP Poly DirectorAI multi-camera automatic switching" (HP newsroom, 3 February 2026). The same post said 5.0 "will be available in February 2026"; the release notes give 5.0.0's release date as May 2026.
Microsoft's certification dates explain some of the pressure on the platform. Microsoft lists, for Teams devices on Android, a certification end date of 3 September 2026 for Android 10 and Android 11 (support ends 3 September 2027), and 15 August 2028 for Android 12 and 13 (support ends 15 August 2030) (Microsoft Learn, last updated 28 September 2026). The Android 10 and 11 date had already been extended once, from 3 September 2025, and the same table lists Android 17 (certification end 16 June 2031), the version HP names as 5.x's future target. HP's notes don't connect 5.0's Teams-first scope to those dates; that link is my inference, and a reasonable question to ask.
Provider modes
A Poly room system runs in Poly Video Mode (Poly's own conferencing app, SIP and H.323) or in Partner Mode, where "Poly Partner Mode allows you to run third-party conferencing applications on supported Poly video systems" (Partner Mode Administrator Guide 4.7.0). The choice is one setting. The configuration guide lists management.provider.select with these values: Poly, Zoom Rooms, Microsoft Teams, GoToRoom, RingCentral Rooms, Device Mode, StarLeaf, BlueJeans Rooms (beta) and Dialpad Conference Room (Poly VideoOS Configuration Parameter Reference Guide 4.1.0, November 2023). That list has moved since 2023:
"As of Poly VideoOS 4.2.0, BlueJeans is no longer supported", and systems still using it "automatically update to Poly Video Mode" (Partner Mode Administrator Guide 4.7.0).
Google Meet arrived on the Android-based Studio X30, X50 and X70 with the 4.0 release: "the Google Meet hardware experience will become available in the upcoming Poly OS 4.0 update" (Google Workspace Updates, 9 March 2023).
Not every provider runs on every system: "Dialpad and Google Meet aren't supported on Poly G7500 systems" and "Poly Studio G62 is certified for Microsoft Teams Rooms, Zoom Rooms, and Tencent only" (Partner Mode Administrator Guide 4.7.0).
Device Mode turns the room system into a USB camera, microphone and speaker for a laptop. It's available alongside a provider (the guide has "Disable Device Mode to stop the system from being used as an external camera, microphone, and speaker") and as a provider of its own.
The mode decides more than the app on screen. Some settings only apply in one mode ("You can't provision this parameter if the system is in Partner Mode"), some features only exist in one (the community FAQ notes a Miracast/AirPlay REST example "will only work in Poly Provider mode"), and the 5.0.0 upgrade itself depends on the mode. For a web UI and API that means capabilities depend on mode as well as model and version.
How the devices are managed
The public guides describe five ways in:
The system web interface. "Open a web browser and enter the system's IP address", sign in as admin; the default password "is the last six characters of your system's serial number", and Poly says to change it. "Unlike the local interface, you can configure every setting through the system web interface." Port 80 "Redirects all sessions to HTTPS on port 443" (Partner Mode Administrator Guide 4.7.0).
Poly Lens. "Poly Lens Cloud provides cloud-based management and insights for your system." A system can be registered during setup or later, and it "remains registered with Poly Lens even after a reset or factory restore." HP describes Lens as "a single management platform" for "settings, firmware updates, and device health" (HP newsroom, 3 February 2026).
A provisioning service such as Poly Clariti Manager. The system registers at an address of the form https://<server>/ucservice, after which "Provisioned settings are read-only in the system web interface" and the system "automatically checks for and runs software updates every time it restarts and at an interval set by the service."
Zero Touch Onboarding. On first boot "the system contacting the Poly Zero Touch Onboarding (ZTO) server to determine its mode of operation". The firewall must allow port 443 to zto.poly.com, lens.poly.com and the software download site, and an NTP server is required. During setup the system tries "Zero Touch Onboarding, Poly Lens, then DHCP" in that order.
The partner's console. In Teams mode, Microsoft says OEM firmware is "distributed through Microsoft Teams Admin Center (TAC) or Teams Rooms Pro management portal (PMP)" (Microsoft Learn, last updated 28 September 2026), and the community post for 5.0.0 says it was delivered "initially via the Microsoft Teams Admin Center".
The documented APIs
REST. The Poly VideoOS REST API Reference Guide (HP document P55757-001A, "Last update: August 2025") covers the G62, G7500, X30, X32, X50, X52, X70 and X72, and describes "the configuration parameters and API commands for Poly VideoOS version 4.4.0". The points worth remembering:
"The REST API feature is never disabled." "Administrator credentials are required", and "You need a session ID using /rest/session/ to pass API commands to the system."
Bodies are JSON, and "The REST API cannot accept more than 10 MB of data."
The guide's sections are Audio, Calendar, Cameras, Collaboration, Conferences, Directory, Session, System, SysTracker (GET /rest/mediastats) and Video; all 107 documented commands are HTTPS with access level Admin. Examples: GET /rest/audio, GET /rest/system/status, POST /rest/system/reboot with {"action": "reboot"}, POST /rest/system/network/ping, GET /rest/system/logs, and GET/POST/DELETE /rest/system/mode/device to read, start and stop Device Mode.
The error table maps codes to meanings, some of them Poly's own: 404 Unavailable ("Operation or source unavailable"), 415 Error ("Payload invalid or missing one or more required fields"), 501 Not Implemented, 507 Insufficient Storage.
Since VideoOS 4.6.2 the REST API wants an XSRF token. The community FAQ for the REST API says "Since PolyOS 4.6.2 added X-XSRF-Token to the REST API to prevent Cross-Site Request Forgery attacks", and its examples log in, keep the session_id and XSRF-TOKEN cookies, and send the token back in an X-XSRF-TOKEN header (community knowledge-base post "[FAQ] Rest API for G7500 and Studio X or Studio G62", 8 January 2026, which says the community "is not the official documentation source and is maintained by volunteers and enthusiasts"). The FAQ's examples send the header on every call, GET /rest/system included; the public material I found doesn't say whether reads without it are refused, so don't claim it's "state-changing calls only". The 4.7.0 release post adds a setting under Security > Access, "Require X-XSRF Authentication Token When Using REST API", on by default, with "HP Poly recommends leaving this setting as enabled."
Command line. The Video Mode Administrator Guide 4.7.0 documents a command-line API over SSH, Telnet or a serial port:
"Use SSH on port 22 if you want encrypted access to the system command-line API" (the setting is "Enable Legacy API Over SSH"). "Use port 24 or 23 to access the system command-line API using telnet", 24 being the default.
Both are off by default: sec.telnet.enable and sec.ssh.enable default to False in the configuration guide, and the port table lists ports 22, 23 and 24 as not open by default.
Telnet asks for a password by default, but "you can't restrict telnet access in other ways, such as locking its port for too many failed login attempts (like you can with web or SSH access)"; a session "disconnects after three failed login attempts."
An RS-232 port in "Control" mode lets "any device connected to the RS-232 port to control the system using API commands"; the Studio G62 and the X-series bars "don't have a serial port." Unlike SSH and Telnet, the serial port isn't off by default: device.serial.mode defaults to "Control" (Configuration Parameter Reference Guide 4.1.0).
The command reference itself is the "Poly G7500 Command-Line API Reference Guide"; I couldn't open a current copy, so this page doesn't list its commands. The community FAQ gives one example, addrbook all, which "will return all the Speed Dials added to the system."
What the public documentation doesn't say
How the web UI, the REST API and the command line are built on the device: which processes serve them, whether they share one command layer, how they reach Android system services.
Anything about "Project Trinity", the team structure, or the plan for the 4.x → 5.x work beyond what the release notes state.
Why the role description's Android 11 differs from the release notes' Android 10 (see the note above).
How the session ID is meant to be sent: the REST guide shows the login response with a sessionId but doesn't say. The community FAQ's examples use the session_id cookie.
When the other providers reach 5.x. HP's February 2026 post names 5.1 and DirectorAI, not a provider schedule.
When an interviewer asks about any of these, say what's public and then how you'd design it, labelled as your design.
The documented REST session, from the outside
Here is the documented login and call sequence, played against a simulated device: a small Express server I wrote that behaves the way the REST guide and the community FAQ describe (session cookie after POST /rest/session, then an XSRF token). The model asks for the token only on state-changing calls, the usual double-submit design; whether the real device also wants it on reads isn't documented. It is not Poly's code, it runs over plain HTTP on localhost where the real device uses HTTPS with its own certificate, and the status codes for the failure cases are my choice.
session-flow.mjs
// The session flow Poly documents for its REST API (POST /rest/session, a session cookie, and since VideoOS 4.6.2// an X-XSRF-TOKEN header), played against a SIMULATED device. The server is a teaching model, not Poly's code.import express from"express";import{ randomUUID }from"node:crypto";const sessions =newMap();// sessionId -> { user, xsrf }constcookies=(req)=> Object.fromEntries((req.headers.cookie ??"").split("; ").filter(Boolean).map((c)=> c.split("=")));const device =express();
device.use(express.json({limit:"10mb"}));// the public guide: the API "cannot accept more than 10 MB"
device.post("/rest/session",(req, res)=>{if(req.body.user !=="admin"|| req.body.password !=="S3cret!")return res.status(403).json({success:false});const sessionId =randomUUID();const xsrf =randomUUID();
sessions.set(sessionId,{user:"admin", xsrf });
res.append("Set-Cookie",`session_id=${sessionId}; HttpOnly; Secure; Path=/`);
res.append("Set-Cookie",`XSRF-TOKEN=${xsrf}; Secure; Path=/`);// readable by the page's script, on purpose
res.json({success:true,session:{role:"ADMIN",isAuthenticated:true}});});
device.use("/rest",(req, res, next)=>{const session = sessions.get(cookies(req).session_id);if(!session)return res.status(403).json({error:"not authenticated"});const unsafe =!["GET","HEAD"].includes(req.method);if(unsafe && req.get("X-XSRF-TOKEN")!== session.xsrf)return res.status(403).json({error:"missing or wrong XSRF token"});next();});
device.get("/rest/system",(req, res)=> res.json({model:"SimulatedBar",softwareVersion:"4.6.2",state:"READY",rebootNeeded:false}));
device.post("/rest/system/reboot",(req, res)=> res.json({accepted: req.body.action ==="reboot"}));const server = device.listen(0,"127.0.0.1");// an explicit address: the port is then free on 127.0.0.1 tooawaitnewPromise((resolve)=> server.once("listening", resolve));// the port is known once it's listeningconst base =`http://127.0.0.1:${server.address().port}`;const jar ={};asyncfunctioncall(method, path, body, headers ={}){const res =awaitfetch(base + path,{
method,headers:{"Content-Type":"application/json",Cookie: Object.entries(jar).map(([k, v])=>`${k}=${v}`).join("; "),...headers },body: body &&JSON.stringify(body),});for(const c of res.headers.getSetCookie()){const[k, v]= c.split(";")[0].split("="); jar[k]= v;}
console.log(`${method.padEnd(4)}${path.padEnd(20)}${res.status}${await res.text()}`);}awaitcall("GET","/rest/system");awaitcall("POST","/rest/session",{user:"admin",password:"wrong"});awaitcall("POST","/rest/session",{user:"admin",password:"S3cret!"});
console.log(` cookie jar now holds: ${Object.keys(jar).join(", ")}`);awaitcall("GET","/rest/system");awaitcall("POST","/rest/system/reboot",{action:"reboot"});awaitcall("POST","/rest/system/reboot",{action:"reboot"},{"X-XSRF-TOKEN": jar["XSRF-TOKEN"]});
server.close();
Result
GET /rest/system 403 {"error":"not authenticated"}
POST /rest/session 403 {"success":false}
POST /rest/session 200 {"success":true,"session":{"role":"ADMIN","isAuthenticated":true}}
cookie jar now holds: session_id, XSRF-TOKEN
GET /rest/system 200 {"model":"SimulatedBar","softwareVersion":"4.6.2","state":"READY","rebootNeeded":false}
POST /rest/system/reboot 403 {"error":"missing or wrong XSRF token"}
POST /rest/system/reboot 200 {"accepted":true}
What’s happening
The first GET /rest/system has no cookie, so the /rest middleware finds no session and answers 403. The REST guide's rule is that you need a session before any command; the code for "no session" on a real device isn't documented, so treat 403 here as the model's choice.
A login with the wrong password gets 403 with success: false; the guide lists "403 LOG-IN ATTEMPT FAILED" for POST /rest/session. With the right password the server creates two random values and sends two cookies: session_id (marked HttpOnly, so page scripts can't read it) and XSRF-TOKEN (deliberately readable, so the web UI's script can copy it into a header). The ids are random on every run, which is why the model never prints them.
The client's little cookie jar now holds both, exactly as curl -c cookies.txt does in the community FAQ. The next GET /rest/system carries the session cookie and succeeds. The response's fields (softwareVersion, state, rebootNeeded) are taken from the FAQ's sample output; model is made up.
POST /rest/system/reboot with only the cookies is refused: it's a state-changing call, and since 4.6.2 the device wants the token in an X-XSRF-TOKEN header too. The browser would attach the cookies automatically to a forged cross-site request, but a page on another origin can't read the XSRF-TOKEN cookie to copy it into a header. That is the double-submit cookie pattern (Securing the on-device web UI).
The last call copies jar["XSRF-TOKEN"] into the header and the reboot is accepted. Notice what 4.6.2 did to every existing integration script: login-then-POST scripts that worked for years started failing until they learned to send the header, which is why 4.7.0 added a switch to turn the requirement off "for systems that can't send X-XSRF tokens". A security fix is also an API change.
AdvancedTrap: vendor error codes aren't standard
?Which public fact rules out a rollback plan?
A colleague proposes: "If 5.0.0 misbehaves in a customer's Teams rooms, Lens can push 4.6.2 back to them." Using only the public release notes, what's wrong with that plan, and what does it change about how you'd release?
Show answer
The notes say a 5.x system can't go back: "Downgrading from Poly VideoOS 5.x to Poly VideoOS 4.x versions isn't supported", and the release updates the factory-restore partition too. Recovery has to be forward: a 5.0.x fix.
What’s happening
The 5.x notes (in both the 5.0.0 and the 5.0.1 sections) do mention one kind of downgrade, "You can manually downgrade your system by choosing a previous version and selecting update now in Poly Lens", but the same list says 5.x to 4.x isn't supported, and the 5.0.0 platform note says "Once you update your system to Poly VideoOS 5.0.0, you cannot downgrade." Read together (my reading; the notes don't spell it out), Lens may move a device between 5.x builds, never back to 4.x.
"This release also updates the system's factory restore partition to Poly VideoOS 5.0.0", so even a factory restore lands on 5.0.0. There's no hidden escape hatch.
That turns the release plan around: every gate has to be passed before a device upgrades, because afterwards the only fix is a new forward build. In practice that means longer soak in early rings, health checks that block promotion, and a fix-forward pipeline that can ship a 5.0.x quickly. 5.0.1 in August 2026, fixing devices that showed "offline in Poly Lens cloud after upgrading", is what that looks like in public.
It also changes the API work: the 5.x build must accept every 4.x script and integration on day one, because a customer whose script breaks can't downgrade to get it back.
#How a web UI, device APIs and back-end services fit together
Added
On a device, "the back end" isn't a cluster in a data centre; it's the services inside the box that own settings and hardware. When an admin clicks Save on the network page of the on-device web UI, the request goes from the browser to the device's web server, through authentication and validation, into a settings service that records the change, and from there to a platform service that actually switches the radio off, usually in another process reached over inter-process communication (on Android, typically a bound service over Binder; see Talking to platform services). Then the result has to come back: to the browser that asked, and to every other screen showing the same setting.
A good mental model: an order in a restaurant kitchen. The waiter (web server) takes the order to the pass; the host has already checked the reservation (auth); the order is checked against the menu (validation); the kitchen manager writes it on the board (settings store, version 7 → 8); the right station cooks it (platform service); the bell rings so every waiter sees the board changed (event); and only then does the waiter tell the customer it's done. If the station can't cook it (the radio is busy), the manager rubs it off the board and the waiter says "not right now, try again in five minutes" (rollback, 503 with Retry-After). What you must never do is tell the customer "done" when the order is only on the board.
That sequence is drawn below. Public Poly documentation doesn't describe the internals of its own devices, so this is the general design of a device's request path, not a description of PolyOS.
One Save click from the admin UI to the radio and back. Solid arrows are calls, dashed arrows are replies and events. The browser hears twice: an event for every open screen, and the HTTP reply for the one that asked.
The model below runs that path in Node: Express is the device's web server and API layer, a settings service stages, applies and commits, and a stand-in for an OS service plays the platform's part. A fake clock adds a cost per step so the timings are readable and identical on every run; the "IPC" is a plain async function call.
trace-request.mjs
// One "Save" click traced through the layers of a device: web server -> auth -> validation -> settings service ->// platform service (a stand-in for an OS service reached over IPC) -> event to the UI. A teaching model.import express from"express";import request from"supertest";let clock =0;// fake milliseconds, so the trace is identical on every runconsttrace=(layer, msg, cost =0)=>{ clock += cost; console.log(` t=${String(clock).padStart(3)}ms ${layer.padEnd(9)}${msg}`);};// The platform side: owns the radio. The web stack never touches hardware directly.const networkService ={wifiEnabled:true,busy:false,asyncsetWifiEnabled(on){trace("platform",`NetworkService.setWifiEnabled(${on}) via IPC`,2);if(this.busy){trace("platform","-> ERROR_BUSY (radio is scanning)",40);throw Object.assign(newError("busy"),{code:"ERROR_BUSY"});}this.wifiEnabled = on;trace("platform","-> OK, radio state changed",120);},};// The settings service: validates, persists, applies, and rolls back if the platform refuses.const store ={values:{"wifi.enabled":true},version:7};const subscribers =[(e)=>trace("event",`-> UI socket: ${JSON.stringify(e)}`,1)];asyncfunctionapplySettings(patch){const before ={...store.values };
Object.assign(store.values, patch);trace("settings",`staged ${JSON.stringify(patch)} (config v${store.version} -> v${store.version +1})`,3);try{if("wifi.enabled"in patch)await networkService.setWifiEnabled(patch["wifi.enabled"]);}catch(err){
store.values = before;trace("settings",`rolled back to v${store.version}`,1);throw err;}
store.version++;for(const s of subscribers)s({type:"settings.changed",version: store.version,keys: Object.keys(patch)});}const app =express();
app.use((req, res, next)=>{ clock =0;trace("web",`${req.method}${req.path} (TLS ended at the on-device server)`);next();});
app.use(express.json());
app.use((req, res, next)=>{if(req.get("Cookie")!=="session_id=abc")return res.status(401).json({title:"Not signed in"});if(req.get("X-XSRF-TOKEN")!=="t1")return res.status(403).json({title:"Bad XSRF token"});trace("auth","session abc -> role ADMIN, XSRF ok",1);next();});
app.patch("/api/v1/settings",async(req, res)=>{const bad = Object.entries(req.body).find(([k, v])=> k !=="wifi.enabled"||typeof v !=="boolean");if(bad)return res.status(422).json({title:"Invalid setting",detail: bad[0]});trace("validate","body matches the settings schema",1);try{awaitapplySettings(req.body);trace("web","200 sent",1);
res.json({version: store.version,restartRequired:false});}catch(err){trace("web","503 sent (retryable)",1);
res.status(503).set("Retry-After","5").json({type:"/problems/platform-busy",title:"Device busy",code: err.code });}});constsave=(body)=>request(app).patch("/api/v1/settings").set("Cookie","session_id=abc").set("X-XSRF-TOKEN","t1").send(body);
console.log("Save #1:");let r =awaitsave({"wifi.enabled":false});
console.log(` => ${r.status}${JSON.stringify(r.body)} wifi=${networkService.wifiEnabled}`);
console.log("Save #2 (radio busy):");
networkService.busy =true;
r =awaitsave({"wifi.enabled":true});
console.log(` => ${r.status}${JSON.stringify(r.body)} wifi=${networkService.wifiEnabled} stored=${store.values["wifi.enabled"]}`);
Result
Save #1:
t= 0ms web PATCH /api/v1/settings (TLS ended at the on-device server)
t= 1ms auth session abc -> role ADMIN, XSRF ok
t= 2ms validate body matches the settings schema
t= 5ms settings staged {"wifi.enabled":false} (config v7 -> v8)
t= 7ms platform NetworkService.setWifiEnabled(false) via IPC
t=127ms platform -> OK, radio state changed
t=128ms event -> UI socket: {"type":"settings.changed","version":8,"keys":["wifi.enabled"]}
t=129ms web 200 sent
=> 200 {"version":8,"restartRequired":false} wifi=false
Save #2 (radio busy):
t= 0ms web PATCH /api/v1/settings (TLS ended at the on-device server)
t= 1ms auth session abc -> role ADMIN, XSRF ok
t= 2ms validate body matches the settings schema
t= 5ms settings staged {"wifi.enabled":true} (config v8 -> v9)
t= 7ms platform NetworkService.setWifiEnabled(true) via IPC
t= 47ms platform -> ERROR_BUSY (radio is scanning)
t= 48ms settings rolled back to v8
t= 49ms web 503 sent (retryable)
=> 503 {"type":"/problems/platform-busy","title":"Device busy","code":"ERROR_BUSY"} wifi=false stored=false
What’s happening
Web server (t=0). The first middleware resets the fake clock and logs the request. On a real device this is where TLS ends: the device's own web server holds the certificate, so everything after this line is inside the box. The browser's PATCH carries only what changed ({"wifi.enabled": false}); partial updates are the norm for settings pages (Partial updates: PATCH and JSON Merge Patch).
Auth and validation (t=1, t=2). The session cookie and the XSRF header are checked before anything else runs, then the body is checked against the schema. Both happen before the settings service is touched, so a bad request costs almost nothing and changes nothing.
Settings service stages the change (t=5). It copies the old values into before, applies the patch and notes "v7 → v8", but doesn't bump the version yet. Then it calls the platform service and waits. This is the important ordering: the store isn't committed until the platform says it's applied.
Platform service (t=7 → 127).setWifiEnabled(false) is the only code that touches the radio, and it takes 120 fake milliseconds, far longer than everything else put together. On a device the slow, failure-prone part is almost always the platform, not the web stack. Once it returns, the settings service commits version: 8 and publishes settings.changed to subscribers: here one UI socket, in reality every open browser tab, the touch controller and the cloud agent (Snapshot plus events).
Response (t=129). Only then does the API send 200 {"version": 8, "restartRequired": false}. Returning the new config version lets the UI detect that a later event is newer than its own save; restartRequired tells it whether to show "restart to apply" (Apply, restart and rollback flows).
The failure path. In Save #2 the radio is "scanning" and the platform throws ERROR_BUSY at t=47. The settings service restores before, logs "rolled back to v8", and the API maps the platform's error code to 503 with Retry-After: 5 and a problem-details body. The last line proves the two sides agree: wifi=false (hardware) and stored=false (store). No event is sent, because nothing changed.
If you moved store.version++ and the event to before the platform call (a tempting simplification), Save #2 would announce "v9, Wi-Fi on" to every screen and then fail, leaving the UI, the store and the radio disagreeing. That's the most common class of bug in device admin UIs: "the page says it's on, the device says it's off."
first contact; reject garbage before it costs anything
Who is calling, session expiry, XSRF, lockout, roles
API layer (shared by REST and the UI's own calls)
one place to audit; same rules for every caller
"Is this value allowed?"
settings layer, from one schema
REST, CLI, provisioning and UI all go through it
"Is this allowed now?" (in a call, during an update, in this provider mode)
settings / command layer
depends on device state, which only the device knows
Applying to hardware or the OS
platform service
owns the resource; may need system permissions the web stack shouldn't have
Telling everyone about the change
settings layer, after commit
events must describe what's true, not what was attempted
Translating platform errors to HTTP
API layer
partners need stable codes; the platform's internal codes can change
AdvancedThe same path from the cloud
The diagram starts at a browser on the LAN. Cloud management reaches the same layers a different way: the device agent holds an outbound connection (polling or a long-lived socket), receives "set wifi.enabled to false" from the cloud, and calls the same settings service. Poly's guides describe this from the outside: the device contacts ZTO, Lens and the update server on port 443, and a registered system "automatically checks for and runs software updates every time it restarts and at an interval set by the service." The design consequence is that the settings service must not care which door a change came through, but it must record it, so the audit log can say "changed by Lens policy" or "changed by admin from 10.1.4.20" (Least privilege and audit logs).
AdvancedFast and slow operations
Turning Wi-Fi off takes a fraction of a second, so the request can wait for it. A reboot, a firmware install, a log bundle or a factory reset can take minutes, and the device may go away in the middle. Those shouldn't hold an HTTP request open: return 202 Accepted with an operation resource the UI can poll or subscribe to (Long-running operations). Poly's own POST /rest/system/reboot is documented as returning 200 OK with no output body, which is the older, simpler style: it acknowledges the request, and the client finds out about the reboot by the device going quiet and coming back.
AdvancedContract between web and platform teams
The contract between the web team and the platform team
In the model the settings service calls networkService.setWifiEnabled() directly. On a real device that line is a boundary between teams: the web and API team owns everything above it, the Android platform team owns the service below it, and they usually ship on different schedules. What makes that boundary workable:
A written interface per platform service: method names, argument types, error codes, whether a call is synchronous, how long it may take. On Android this is often literally an AIDL file; whatever the form, both teams review changes to it.
A fake of every platform service that the web team can run in Node or on an emulator, with switches for the failure cases (busy, timeout, permission denied). The model's busy flag is the smallest version of that, and it's how the 503 path got tested at all.
Error codes owned by the API layer. The platform's ERROR_BUSY is mapped to the API's stable platform-busy problem type in one place, so a platform refactor can't change what partners see.
Timeouts at the boundary. The settings service gives each platform call a budget and turns "no answer" into a clear error, instead of letting a browser spinner run until the HTTP request dies.
An integration environment where both sides' latest builds meet daily, so a contract break shows up in a day, not at release. Working across SDK, platform and web workstreams goes into this for a distributed team.
?"It saved, but after a reboot it's back to the old value"
An admin changes the device's time zone in the web UI, sees the success toast, and the clock updates. After the nightly scheduled restart the old time zone is back. The API returned 200. Which layer do you suspect first, and how do you check?
Show answer
Suspect the settings layer's persistence (or a provisioning layer overriding it), not the web UI or the platform service: the platform applied it, so the value reached the OS, but whatever is read at boot wasn't updated.
What’s happening
The clock changing right away proves the platform service ran: the request got through auth, validation and the call into the OS. So the web UI and the API layer did their jobs.
Coming back after a restart means the value the device boots from wasn't the one that was applied. Either the settings service applied it to the OS without persisting it (in the model's terms, it changed the radio but never committed the store), or it persisted it in a layer that loses on boot.
Check the precedence first: if the device is registered with a provisioning service that sets the time zone, the provisioned value wins at the next sync or boot. Poly documents exactly this ("Provisioned settings are read-only in the system web interface"), and a correct UI would have shown the field as read-only. A UI that let the admin edit a provisioned field is a second bug.
Then reproduce: set the value, read it back through a different door (the CLI or GET on the settings API), reboot, read again, and look at the device log for the commit and for any provisioning sync after boot. If the read-back through another door already shows the old value, it was never persisted; if it shows the new one until a sync, provisioning overrode it.
The fix depends on which: commit before reporting success (and test it with a restart in the test suite), or make provisioned fields read-only in the UI and explain why ("managed by your provisioning server").
#Migrating a legacy device app to a new OS version
Added
An OS migration on a device platform means moving everything that runs on the old OS (the web UI server, the APIs, the device agent, the platform integrations, the partner apps) to a new OS version, and then moving the fleet. On this role's platform that's PolyOS 4.x to 5.x, with the Android version underneath jumping several releases at once (the public 5.0.0 notes say Android 10 → 13 on the second-generation systems). The work has three parts that interviewers will probe: parity (does everything that worked before still work?), compatibility (do old scripts, integrations, settings and peripherals still work against the new build?) and phased rollout (how do you move tens of thousands of devices without breaking meeting rooms, when the upgrade can't be undone?).
A good mental model: moving a family to a new house while they keep living their lives. Before moving you list everything they use, including the things nobody wrote down (the spare key under the mat). You check that every key, appliance and plug works in the new house. You move one room at a time and make sure people can still eat and sleep at every step. And because the old house is sold the moment you hand over the keys, you can't move back; if something's wrong you fix it in the new house.
HP's public release notes show the shape of exactly this kind of migration, drawn below: a bridge release that every device must reach first, a new OS for the newer hardware only, one supported mode at launch while the old train keeps shipping for everyone else, and a one-way door.
Poly VideoOS release trains in 2026, from HP's public release notes and announcement (not to scale). Three groups of devices on two trains: the old train keeps shipping while the new OS reaches one mode on the newer hardware first.
What the public notes say, step by step, and the general practice each one illustrates:
Public fact (HP release notes, 2026)
The general practice
"Your system must be running Poly VideoOS 4.6.0 before updating to Poly VideoOS 5.0.0", and older systems load 4.6.0 automatically first
a bridge release: one known starting point, so the upgrader and the data migrations only have to handle one source version
5.0.0 "supports only Microsoft Teams and Poly Device Mode"
phase by feature: ship the new OS where parity has been proven first, keep the rest on the old train
4.7.0 (June 2026) is a sustaining release "for all Poly and partner modes"
two trains at once: fixes and security updates continue on the old OS while the new one ramps
"Partner applications are no longer bundled in the main software package"
decouple what changes at different speeds: the OS image and the partner apps release separately
"Downgrading from Poly VideoOS 5.x to Poly VideoOS 4.x versions isn't supported", and the factory-restore partition is updated too
a one-way door: every gate must be passed before devices cross it; recovery is fix-forward
5.0.1 fixes "the device shows as offline in Poly Lens cloud after upgrading"
management connectivity is part of parity: a device you can't see after the update is a failed update
What changes between Android 10/11 and 13
A multi-version jump collects every behaviour change in between. Android's own lists (Android Developers, "Behavior changes" pages for Android 11, 12 and 13, last updated 23 October 2026) include, among many others:
Android 11: package visibility ("When an app queries for the list of installed apps on the device, the returned list is filtered"; apps declare what they need with <queries>) and scoped storage enforcement.
Android 12 (for apps targeting it): components with intent filters must declare android:exported, or "your app can't be installed"; every PendingIntent must specify its mutability; apps "can't start foreground services while running in the background," with a few exceptions.
Android 13: a runtime notification permission, POST_NOTIFICATIONS, and a Task Manager from which "users can complete a workflow from the notification drawer to stop apps that have ongoing foreground services."
Many of these apply when an app targets the newer API level, and a device vendor's own system apps are built and signed differently from Play Store apps, so which rules bite depends on how each component is built. The public Poly docs don't say how PolyOS's components are packaged. Android 11 to 13: changes that matter goes through them for a device platform.
The web UI and API side of a migration
For the workstreams this role owns, a migration plan usually has these parts:
An inventory of the surface. Every REST endpoint, CLI command, setting name, event and default, including undocumented ones customers use. HP's community FAQ shows one: GET /rest/system/bluetooth returns the remote's battery level, with the warning "this is working at present but could change at any point. If you need this ensured please work with Poly sales to request this to be documented." Somebody's script depends on it.
Recorded fixtures and a parity check. Record real requests and responses from the old build and replay them against the new one (the model below).
Settings migration. The new build reads settings written by the old one on first boot. Migrations are numbered, idempotent and keep unknown keys (the second model).
Contract tests between teams. The web UI, the platform services, the SDK and partner integrations each get tests against the agreed API contract, so a change on one side fails a build, not a customer (Contract testing).
Rollout rings with health gates. Lab, internal, a small percentage, wider rings, each promoted only if its health holds (the third model).
The first model is the parity check. Recorded responses from the old build are replayed against the new build, and each difference is classified by shape (field names and types), not by value, because values like version and uptime are supposed to differ. The endpoints and builds are invented for the model.
parity-check.mjs
// Parity check for an OS migration: replay recorded requests against the old (4.x) and new (5.x) builds of the// same device API and classify every difference. Endpoints and builds are invented for the model.constshape=(v)=>
Array.isArray(v)?[shape(v[0])]: v &&typeof v ==="object"? Object.fromEntries(Object.entries(v).map(([k, x])=>[k,shape(x)])):typeof v;// Responses recorded from the old build, and what the new build returns for the same requests.const oldBuild ={"GET /api/v1/system":[200,{model:"X",version:"4.6.2",uptime:17250}],"GET /api/v1/audio":[200,{muted:false,volume:62}],"GET /api/v1/network/wifi":[200,{enabled:true,ssid:"Corp"}],"POST /api/v1/system/reboot":[200,{result:true}],"GET /api/v1/system/logs":[200,{url:"/logs.tar"}],};const newBuild ={"GET /api/v1/system":[200,{model:"X",version:"5.0.0",uptime:12,androidVersion:"13"}],"GET /api/v1/audio":[200,{muted:false,volume:"62"}],"GET /api/v1/network/wifi":[200,{enabled:true,networkName:"Corp"}],"POST /api/v1/system/reboot":[202,{operationId:"op_1"}],};functioncompare(a, b, path =""){const issues =[];if(typeof a !=="object"|| a ===null)return a === b ?[]:[`${path.slice(0,-1)||"body"}: ${a} -> ${b} (BREAKING)`];for(const k of Object.keys(a)){if(!(k in b)) issues.push(`${path}${k}: removed (BREAKING)`);else issues.push(...compare(a[k], b[k],`${path}${k}.`));}for(const k of Object.keys(b))if(!(k in a)) issues.push(`${path}${k}: added (compatible)`);return issues;}let breaking =0;for(const[call,[oldStatus, oldBody]]of Object.entries(oldBuild)){if(!newBuild[call]){ console.log(`MISSING ${call}`); breaking++;continue;}const[newStatus, newBody]= newBuild[call];const issues =[...(oldStatus !== newStatus ?[`status: ${oldStatus} -> ${newStatus} (BREAKING)`]:[]),...compare(shape(oldBody),shape(newBody)),];const isBreaking = issues.some((i)=> i.includes("BREAKING"));
breaking += isBreaking;
console.log(`${isBreaking ?"BREAKING ": issues.length ?"OK+ADDED ":"SAME "}${call}${issues.map((i)=>`\n ${i}`).join("")}`);}
console.log(`${Object.keys(oldBuild).length} recorded calls, ${breaking} need a decision before 5.x ships`);
Result
OK+ADDED GET /api/v1/system
androidVersion: added (compatible)
BREAKING GET /api/v1/audio
volume: number -> string (BREAKING)
BREAKING GET /api/v1/network/wifi
ssid: removed (BREAKING)
networkName: added (compatible)
BREAKING POST /api/v1/system/reboot
status: 200 -> 202 (BREAKING)
result: removed (BREAKING)
operationId: added (compatible)
MISSING GET /api/v1/system/logs
5 recorded calls, 4 need a decision before 5.x ships
What’s happening
shape turns a response body into its skeleton: { model: "X", version: "4.6.2", uptime: 17250 } becomes { model: "string", version: "string", uptime: "number" }. Comparing skeletons ignores values that should change between builds (versions, uptimes) and catches the ones that matter: names and types.
GET /api/v1/system keeps every old field with the same types and adds androidVersion. That's compatible: a tolerant reader ignores fields it doesn't know. It's reported as OK+ADDED so the docs and the SDK get updated, but it doesn't block the release.
GET /api/v1/audio returns volume as "62" instead of 62. Nobody renamed anything, so a quick manual check would pass it, but a partner's volume + 5 now produces "625". Type changes are the sneakiest breaking change, usually caused by a new serialiser or a field read from a different system API on the new OS.
GET /api/v1/network/wifi renamed ssid to networkName. The new field is fine; removing the old one breaks every reader of ssid. The compatible fix is to send both in the new build and deprecate ssid on a published schedule (Deprecation and sunset).
POST /api/v1/system/reboot changed from 200 { result: true } to 202 { operationId }. The 202 is the better design, but old scripts check for 200 and result. Changes like this belong in a new API version or behind a capability the client asks for, not in-place in a migration. And GET /api/v1/system/logs is missing entirely: probably not ported yet, and the most important line on the report.
The last line is the release gate: 4 of 5 recorded calls need a decision. In a real migration this runs in CI against thousands of recorded calls from the old build, and the report becomes a burn-down list: each item is either fixed for parity or turned into a documented, approved change.
AdvancedMigrating stored settings
The new build has to boot from settings the old build wrote. The safe pattern is a list of numbered migrations, each run once, with the store recording which number it has reached, and keys the migration doesn't recognise kept rather than dropped (they may belong to a partner app or a newer feature). The setting names and steps here are invented for the model.
migrate-config.mjs
// Migrating a device's stored settings from the old OS release's format to the new one, as numbered steps.// Each step runs once (the stored schemaVersion says which have run), and keys it doesn't know are kept, not dropped.const migrations =[{to:2,why:"rename ssid -> wifi.networkName",run:(s)=>{if("ssid"in s){ s["wifi.networkName"]= s.ssid;delete s.ssid;}}},{to:3,why:"volume stored as a number",run:(s)=>{if(typeof s["audio.volume"]==="string") s["audio.volume"]=Number(s["audio.volume"]);}},{to:4,why:"provider 'Poly' is now 'videoMode'",run:(s)=>{if(s.provider ==="Poly") s.provider ="videoMode";}},];functionmigrate(stored){const s =structuredClone(stored);const from = s.schemaVersion ??1;for(const m of migrations){if(m.to <= from)continue;
m.run(s);
s.schemaVersion = m.to;
console.log(` v${m.to -1} -> v${m.to}: ${m.why}`);}if(s.schemaVersion === from) console.log(" already current, nothing to do");return s;}const fromOldDevice ={schemaVersion:1,ssid:"Corp","audio.volume":"62",provider:"Poly","partner.zoom.roomId":"r-17"};
console.log("first boot on the new OS:");const once =migrate(fromOldDevice);
console.log(" ",JSON.stringify(once));
console.log("second boot (or the migration is retried after a crash):");const twice =migrate(once);
console.log(` same result: ${JSON.stringify(once)===JSON.stringify(twice)}`);
console.log(` unknown key kept: partner.zoom.roomId = ${twice["partner.zoom.roomId"]}`);
Result
first boot on the new OS:
v1 -> v2: rename ssid -> wifi.networkName
v2 -> v3: volume stored as a number
v3 -> v4: provider 'Poly' is now 'videoMode'
{"schemaVersion":4,"audio.volume":62,"provider":"videoMode","partner.zoom.roomId":"r-17","wifi.networkName":"Corp"}
second boot (or the migration is retried after a crash):
already current, nothing to do
same result: true
unknown key kept: partner.zoom.roomId = r-17
What’s happening
The stored settings from the old build say schemaVersion: 1. migrate skips every step whose to is at or below that and runs the rest in order: 1 → 2 renames ssid, 2 → 3 turns "62" into 62, 3 → 4 renames the provider value. Each step updates schemaVersion as it finishes, so a crash halfway leaves a record of how far it got.
Each step is written to be safe if it runs on data it has already changed (if ("ssid" in s), if (typeof … === "string")). That's belt and braces on top of the version check, because on a device a power cut during first boot is a real scenario.
partner.zoom.roomId isn't mentioned by any step and comes through untouched. A migration that rebuilt the store from a list of known keys would silently delete it, and the room would lose its Zoom registration on upgrade.
The second run starts at schemaVersion: 4, runs nothing and returns an identical object (same result: true). Idempotent migrations are what make retries, crashes and "the device rebooted during the upgrade" survivable.
Note the order of keys in the output: wifi.networkName moved to the end because it was added after the delete. Nothing should depend on key order, and the parity check above compares by key, not position.
AdvancedRolling out a one-way upgrade
The last part is moving the fleet. Rings grow in size, and each ring is promoted only if its health holds. This model draws each device's outcome from a seeded PRNG, so it's reproducible; the ring sizes, thresholds and failure rates are illustrative, not HP's.
ring-rollout.mjs
// A phased rollout of a one-way OS upgrade through rings, with a health gate after each ring.// Each device's outcome is drawn from a seeded PRNG (mulberry32), so the run is reproducible.functionmulberry32(seed){return()=>{
seed |=0; seed =(seed +0x6d2b79f5)|0;let t = Math.imul(seed ^(seed >>>15),1| seed);
t =(t + Math.imul(t ^(t >>>7),61| t))^ t;return((t ^(t >>>14))>>>0)/4294967296;};}const rings =[{name:"lab",devices:40},{name:"internal",devices:200},{name:"early 2%",devices:400},{name:"10%",devices:2000},{name:"50%",devices:8000},];// Gate: stop if more than 1% of upgraded devices fail to come back online, or more than 2% lose a peripheral.const gate ={offline:0.01,peripheral:0.02};functionrollout(build){const rand =mulberry32(build.seed);
console.log(`build ${build.name}`);for(const ring of rings){let offline =0, peripheral =0;for(let i =0; i < ring.devices; i++){if(rand()< build.offlineRate) offline++;elseif(rand()< build.peripheralRate *(ring.name ==="lab"?0:1)) peripheral++;// lab rigs only carry current peripherals}const o = offline / ring.devices, p = peripheral / ring.devices;const pass = o <= gate.offline && p <= gate.peripheral;
console.log(`${ring.name.padEnd(9)}${String(ring.devices).padStart(5)} devices offline ${(o *100).toFixed(2)}% peripheral lost ${(p *100).toFixed(2)}% ${pass ?"PASS":"HALT"}`);if(!pass)return console.log(` halted: the upgrade is one-way, so these ${ring.devices} devices need a fix-forward build, not a rollback`);}
console.log(" promoted to 100%");}rollout({name:"v5-rc1",seed:7,offlineRate:0.002,peripheralRate:0.05});rollout({name:"v5-rc2",seed:7,offlineRate:0.002,peripheralRate:0.004});
Result
build v5-rc1
lab 40 devices offline 0.00% peripheral lost 0.00% PASS
internal 200 devices offline 0.50% peripheral lost 5.50% HALT
halted: the upgrade is one-way, so these 200 devices need a fix-forward build, not a rollback
build v5-rc2
lab 40 devices offline 0.00% peripheral lost 0.00% PASS
internal 200 devices offline 0.50% peripheral lost 1.00% PASS
early 2% 400 devices offline 0.00% peripheral lost 0.25% PASS
10% 2000 devices offline 0.25% peripheral lost 0.65% PASS
50% 8000 devices offline 0.27% peripheral lost 0.61% PASS
promoted to 100%
What’s happening
Five rings, each bigger than the last: 40 lab devices, 200 internal rooms, then 2%, 10% and 50% of a customer fleet. The gate after every ring checks two health signals: devices that don't come back online after the update (more than 1% halts) and devices that lose a paired peripheral (more than 2% halts).
Build v5-rc1 has a peripheral problem (5% of real rooms lose one), but the lab doesn't see it: the model's lab rigs only carry current peripherals, so their peripheral failure chance is zero and the lab ring passes with 0.00%. That is the lab-vs-field gap from the first topic, in two lines of output.
The internal ring, real rooms with real, older accessories, shows 5.50% peripheral loss and halts. Because the upgrade can't be undone, those 200 devices stay on the new build and wait for a fix-forward build. Halting at 200 internal rooms instead of 8,000 customer rooms is the whole point of the rings.
v5-rc2 uses the same seed, so the random draws line up with rc1's and the only difference is the fixed peripheral rate. Every ring passes and the build is promoted. Real rings also include soak time (days per ring, so problems that appear after the nightly restart show up) and per-segment checks (model, provider mode, peripheral mix), which a model this small leaves out.
Choosing the signals is the engineering. "Offline after update" catches bricked devices and broken management connections (the kind of issue 5.0.1 fixed for Lens); "peripheral lost" catches pairing and driver regressions. Crash rate, call-join success and API error rate from the new build are the next ones you'd add.
?What does the bridge release buy you?
HP requires every device to be on 4.6.0 before it can take 5.0.0 and installs 4.6.0 first if needed. Why would a platform team impose a bridge release rather than let 5.0.0 upgrade from any 4.x?
Show answer
It shrinks the problem from "every 4.x version to 5.0" to "4.6 to 5.0": one source format for settings, one known upgrader, one set of starting conditions to test.
What’s happening
Without a bridge, the 5.0 installer and its data migrations would have to handle every older 4.x layout of settings, partitions and partner app data that's still in the field, and the test matrix would include every one of those starting points.
With a bridge, 4.6.0 can do preparatory work on the old OS that's easier there: migrate settings into a format 5.0 understands, check free space, update the update agent itself, refuse if the device is in a mode 5.0 doesn't support.
The release notes' wording backs this: "Updating to this release without first upgrading to Poly VideoOS 4.6.0 or later isn't supported, as it's an untested upgrade path that may result in your system not functioning correctly." Untested is the key word: the bridge exists so the tested path is the only path.
The cost is time: devices far behind take two updates, and the 4.6 bridge itself has to be rolled out first. That's usually worth it for a one-way upgrade, where a failure can't be rolled back.
This role sits where three worlds meet: the browser (an admin web UI in React or Angular), the API (REST and Telnet/command-line interfaces that partners and scripts depend on for years), and the platform (Android services, the SDK and the platform teams who own them), with a team of developers in India to guide. Interviewers for a role like this aren't checking whether you've worked at a device company. They're checking whether you can reason about the things that make device work different, own an API that can't break, work across team boundaries without being blocked, and lead people you mostly see on video calls.
A good mental model for your own pitch: you're the translator. The platform team speaks Android services, permissions and release trains; the web team speaks components, forms and HTTP; partners speak "my script broke"; the India team needs clear contracts and fast answers across a time-zone gap. Your story is that you've worked in more than one of those languages and you turn the conversations between them into contracts, tests and decisions. Every claim in that story has to be something you've actually done: this topic gives structure and prompts, never invented experience.
What they'll probe
Area
What they're testing
How to show it
Web UI for hardware
Can you build admin UIs that cope with devices that reboot, go offline and refuse changes?
talk about state you couldn't trust: optimistic updates rolled back, reconnects, forms driven by a schema (What device admin UIs must do well)
REST API ownership
Do you design for compatibility, not just correctness?
Can you work with OS and SDK teams without being one?
what an Android app can and can't do, permissions and lifecycle, debugging with adb and logcat; honesty about system-level work you haven't done
The 4.x → 5.x migration
Can you plan a migration you can't roll back?
inventory, fixtures, contract tests, settings migrations, rings and gates
Technical leadership
Can you make a distributed team productive?
clear contracts and mocks so nobody waits, reviews that teach, decisions written down, overlap hours used for unblocking
Judgement
Do you know what you don't know?
saying "the public docs don't say; here's how I'd find out" instead of bluffing
Mapping your background to the role
What's true and stated in your brief for these sites: you're an experienced full stack engineer (React, Angular, Node and databases) and you build Android apps. Map each to the role, then fill in your own examples:
React and Angular → the admin web UI. Settings forms, validation, status dashboards, error and offline states, accessibility. The device-specific twist to talk about: the server is the device, and it can disappear mid-save.
Node → the API layer and tooling. REST services, validation, auth, line-based servers with the net module, test harnesses, device simulators. Every model on this site is Node; once you've run and changed them yourself, you can say you've worked through small working models of these patterns (preparation, not work experience; see the box below).
Databases → the config store and history. Schemas, migrations, idempotency, audit trails. Settings migrations on first boot are schema migrations under harsher conditions.
Android apps → the platform boundary. You know the app side of the line: manifests, permissions, services, lifecycle, adb, logcat. Say plainly where the line is: system apps, platform signing, AIDL services owned by an OS team and OTA pipelines are things you understand conceptually and would learn on the job, unless you've done them.
The answer skeleton below is a template, not an answer. Everything in square brackets is for you to fill with what you actually did; delete any line you can't back with a real example.
Answer skeleton: "Why are you a fit for this role?"
1. One line on who I am: full stack engineer, [years] years, [React/Angular/Node/databases], also build Android apps.
2. The bridge to devices: "The part of my work closest to this role is [a real project] where [the server / the
client / the device] was something I couldn't fully control: [what made it hard]."
3. A decision I made: "I chose [design] over [alternative] because [constraint], and the result was [outcome,
number if you have one]."
4. The Android side: "On Android I've built [real apps or features], so I know [manifests, services,
permissions, adb, logcat]. Platform-level work like [system services, OTA] I know at the
design level, and I'd lean on the platform team there."
5. Leading people: "I've [mentored / reviewed / led] [who], and what worked was [a concrete practice]."
6. Why this role: "I want to own the web UI and API layer on a platform where compatibility and reliability
matter, and the 4.x to 5.x migration is exactly that kind of problem."
What’s happening
Line 1 is ten seconds. Its job is to set the frame (full stack plus Android), not to list a CV. Keep the numbers real; if you don't remember the exact years, round down.
Line 2 is the most important line, and it has to be a real project. Device work is about not controlling the runtime, so pick something where you didn't: a mobile app on users' phones, a client you had to keep compatible, a service other teams depended on, a deployment to machines you couldn't log into. Say what made it hard in one clause.
Line 3 turns the story into judgement. Interviewers remember a trade-off ("I kept the old field and added the new one, because three teams parsed it") far longer than a technology list. Prepare two or three of these from your own work, ideally one about an API and one about a UI.
Line 4 is where honesty earns trust. You genuinely build Android apps, so say what that covers; then name the platform-level things you haven't done and how you'd handle them. Interviewers for this role will know the difference between app and platform work, and an accurate boundary beats a vague claim.
Line 5 only if you have a real example of mentoring, reviewing or leading. If your leadership experience is informal (onboarding a colleague, writing the team's conventions), say that; the Tech Lead site has structures for these answers.
Line 6 ties back to their problem, using what you know from public sources: the role's own description and the public release notes. Don't claim knowledge of their internal plans.
AdvancedMaterial from your own repositories
Material from your own work to look at
These are prompts, not answers. Each names something in your own repositories that touches the themes of this role; whether and how you use it is yours to decide. Check the details yourself, and describe your own part in your own words: what you did, what was hard, what you'd do differently. Don't borrow outcomes or numbers from this page.
Apps that update themselves from a manifest. Your RohitDownloads site keeps a *-latest.json update manifest for several of your apps, each app's release-notes page records versions, downloads and SHA-256 checksums, and Gesture Control's Updates screen checks for and installs the listed apps. In the abstract that's a field-update channel. Prompt: what was your part in it, and what goes wrong when apps in the field are on old versions?
Talking to a device you can't touch. Gesture Control has a Wear OS watch helper and a phone relay that bring a watch's wireless-debugging port within reach of a computer. In the abstract that's a device agent and a relay. Prompt: describe your part in it, not this analogy.
An Android accessibility service. Clipboard's tap-to-paste depends on an accessibility service the user has to switch on, and Android switches it off again if the app is force-stopped. Prompt: what did working with a permission-gated system component teach you beyond a plain activity?
CI/CD for Android. Your Android CI/CD notes describe a pipeline that builds, checks, signs and publishes APKs to GitHub Releases. Prompt: a question about building, signing and releasing.
These study sites. They contain runnable models of device APIs, Telnet servers, versioning and migrations. Once you've run and changed them yourself, you can say "I've worked through small working models of…" if it comes up; it's preparation, not experience, so never "at work I…".
AdvancedQuestions to ask them
Good questions show you've read the public material and you think about the problems they actually have:
"The public 5.0 notes say the Gen2 systems go from Android 10 to 13; the role description says Android 11. Which baseline is the 4.x work on?"
"5.0 shipped for Teams and Device Mode first. How do you decide which provider modes come next, and what does parity mean for the web UI and API in each?"
"Do the REST API, the command-line API and the web UI share one command layer on the device, or have they grown separately?"
"How do the web and API teams work with the Android platform team day to day: shared interface definitions, mocks, an integration lab?"
"How is the India team organised, how much working-day overlap is there, and what do they own end to end?"
"What does the lab and rollout pipeline look like for a 5.x build: rings, soak time, which health signals block promotion?"
"When an XSRF-style security change breaks partner scripts, as 4.6.2 did, how is that communicated, and who decides on an escape hatch like 4.7.0's switch?"
AdvancedTrap: bluffing and claimed inside knowledge
?How would you answer "Tell me about Telnet"?
An interviewer asks: "Our devices still have a Telnet API. Have you worked with Telnet, and what do you think of it?" You've never shipped a Telnet interface at work, but you've studied it. Build the answer.
Show answer
Be exact about your experience, then show understanding: what Telnet is, why devices keep it, how Poly's public docs configure it, the risk, and how you'd build and secure it.
What’s happening
Start with the honest boundary: "I haven't shipped a Telnet interface in production. I've built line-based command servers in Node and studied the protocol." If the second sentence isn't true yet, write one yourself using Telnet, SSH and Command-Line Control first, so it is; reading the site's model isn't the same as building one, and until you have, say "I've studied the protocol and worked through a runnable model".
Then the substance: Telnet is a plain-text, line-oriented protocol over TCP with option negotiation; devices keep it because room control systems and years of integrator scripts speak it, and it's trivial to drive from anything.
Show you've read their docs: "On your systems the command-line API is on port 24 by default or 23, with SSH on 22 as the encrypted option; both are off by default, and Telnet has no port lockout, just a disconnect after three failed logins."
The trade-off: Telnet sends credentials and commands in clear text, so it should stay off by default, be limited to trusted networks when used, and be steered towards SSH; that's consistent with how the public docs set the defaults.
The design you'd bring: one command layer behind REST, SSH and Telnet so behaviour and validation match; machine-readable responses; scripted session tests; fuzzing the parser. That's a strong answer from someone who hasn't shipped Telnet, as long as every sentence in it is true.