APIs & Devices NotesRohit’s interview study guide
Chapter 13

Review Questions

Rapid revision before the interview: questions an interviewer for a device-platform full stack role might ask, with answers you can say out loud in a minute, each linked to the topic that explains it in depth.

5 topics

#Device API design questions

Added

This page is for the last few days before an interview, when you no longer need to learn a topic but need to say it well, quickly. Every question here is one an interviewer for this role might ask (web UI and REST/Telnet APIs for a video-conferencing device platform, a team in India, an OS migration underneath), or one you should be able to answer for yourself in the middle of a design discussion. The first paragraph of each answer is the version you'd say: 30 to 90 seconds, the decision first and the reason straight after. The walkthrough under it is the reasoning you need when the follow-up is "why?", and the link at the end goes to the topic that teaches it properly. Every answer agrees with the chapter it links to; product facts about Poly are only the public ones those chapters cite.

Think of it as a deck of flashcards with the working shown. Read the question, answer it out loud in your own words, then open the answer and compare: did you name the decision, the mechanism or number behind it, and the trade-off? If one of the three is missing, that's the card to go back to. The few questions where arithmetic or behaviour matters have small runnable models; their output was produced on Node 26 and is re-checked every time the site is validated.

This first group covers the API itself and the UI that is its first client: modelling a device as resources, methods and status codes, PATCH, long-running operations, ETags, errors, bulk configuration, constrained devices, OpenAPI from spec to contract test, and the admin web UI's forms, apply flows, updates and offline states.

How would you model a meeting-room video bar as REST resources?
Show answer

Three drawers, plus identity and collections: what the admin chose, what the device reports, and what you asked it to do. "/device for identity, read-only; /settings for configuration, split into sections like /settings/audio and /settings/network with GET and PATCH; /status for reported state, read-only, because it changes every second and nobody sets it; collections for things with their own lifecycle, /peripherals created by the device and /calls created by clients dialling; and /operations for reboots and updates, which come back as 202 with an operation to poll. Then the cross-cutting rules: one error format, the major version in the path, units in field names, stable peripheral ids across reboots."

What’s happening
  1. Settings, status and operations have different writers, change rates, permissions and persistence: an admin writes settings rarely and they survive reboots and exports; the device writes status constantly; operations take time and may fail. AWS IoT's Device Shadow makes the same split with desired and reported.
  2. Singletons (/settings, /status, /device) have no id and support GET and PATCH, never POST or DELETE: you can't create a second set of settings.
  3. Placing a call is creating a resource: POST /calls returns 201 and a Location; DELETE /calls/{id} hangs up, and a second DELETE returning 404 means "already done".
  4. Naming rules save partners pain for years: lowerCamelCase, units in the name (sleepAfterMinutes, temperatureC), enums as lowercase strings, RFC 3339 UTC timestamps.
  5. The two designs to avoid are RPC paths (POST /setVolume) and one giant /config mixing settings, status and identity, whose ETag changes every time the temperature does.

Learn more: Modelling a device as resources.

Why keep settings and status as separate resources? Isn't one /config simpler?
Show answer

Because they have different writers and different lifetimes, and mixing them breaks concurrency, auditing and export. "If live state like the temperature or the mute button sits in the settings document, its ETag changes every few seconds, so every conditional write fails; pressing mute shows up in the audit log as a configuration change; and a configuration import wipes runtime state. Live mute is runtime state you can write, so it gets its own resource, PUT /audio/mute with true, while 'mute when a call starts' is a setting, muteOnJoin under /settings/audio."

What’s happening
  1. Ask the three-drawer question of each field: did an admin choose it, does the device report it, or is it something you do?
  2. An ETag over a document that includes changing status is useless for If-Match: the tag has moved on before the admin's write arrives.
  3. Settings are saved to flash, exported and audited; runtime state must not be. One resource can't have both behaviours.
  4. Writable runtime state is fine as its own resource, written as absolute state (PUT … true) so a retried request can't toggle it back.
  5. For comparison, Poly's public VideoOS REST guide keeps GET/POST /rest/audio/muted separate from configuration.

Learn more: Modelling a device as resources and Optimistic concurrency with ETags.

Which method for each: set the volume to 40, volume up, mute, place a call, hang up, reboot?
Show answer

PUT for absolute state, POST for actions, and an idempotency key on every POST you can't undo. "Volume 40 and mute are PUTs: they say what the state is, so a retry is harmless. Volume up is a relative POST; a lost response plus a retry adds 20 instead of 10, so I keep relative actions for interactive remotes only. Placing a call and rebooting are POSTs that aren't idempotent, so they carry an Idempotency-Key and the device replays the first answer. Hanging up is DELETE /calls/{id}, and a second DELETE that gets 404 counts as done."

What’s happening
  1. RFC 9110 defines GET, PUT and DELETE as idempotent and says clients shouldn't automatically retry non-idempotent methods, so the method choice is the retry policy.
  2. On devices lost responses are routine: Wi-Fi drops, proxies time out, the device reboots itself. Design so the second copy of every request is harmless.
  3. The chapter's model shows it: a reboot with no key scheduled two reboots; with Idempotency-Key: 7c1e the retry replayed {"operation":"op-3","replayed":true}; POST volume-up ended at 60 when one press meant 50.
  4. The Idempotency-Key header is still an IETF draft (draft-ietf-httpapi-idempotency-key-header-08, 22 October 2026), not an RFC; it suggests 400 for a missing key, 422 for a key reused with a different body and 409 while the first request is still running.
  5. Device twist: the key store must survive the reboot the request caused, so keep it in flash with the operation record, and keep keys for minutes to hours, not a payment API's day.

Learn more: Methods, status codes and idempotency for device APIs.

The same POST /firmware-updates can correctly return 503, 409 or 202. When is each right?
Show answer

It depends on the device's state, which is why partners need these codes documented and stable. "503 with Retry-After while the device is booting: the HTTP server is up but the services behind it aren't. 409 Conflict while it's in a call: the request is valid but conflicts with the current state, and the client can fix that by waiting or scheduling. 202 Accepted with a Location for the operation once it's idle. Not 423 Locked, which is WebDAV's lock model, and not 500, which sends clients into error handling instead of waiting."

What’s happening
  1. Booting: a middleware in front of every route answers 503 + Retry-After: 5. RFC 9110 defines Retry-After for exactly "how long the service is expected to be unavailable".
  2. In a call: 409 is "a conflict with the current state of the target resource" that "the user might be able to resolve". Add a stable problem type such as …/device-busy so a fleet tool can reschedule automatically.
  3. Idle: 202 plus an operation resource, because the update takes minutes and outlives the connection.
  4. Others worth knowing: 405 + Allow for a method a resource doesn't support (Express 5 needs an .all() fallback to produce it), 412/428 for preconditions, 413 for oversized bodies, 415 for the wrong patch media type, 422 for well-formed but invalid values, 429 + Retry-After for rate limits.
  5. If you'd rather queue the update until the call ends, 202 is a valid design, but say so in the operation's status instead of silently delaying.

Learn more: Methods, status codes and idempotency for device APIs.

PUT or PATCH for device settings?
Show answer

PATCH with JSON Merge Patch (RFC 7396, application/merge-patch+json), because an old client's PUT silently deletes settings it doesn't know about. "A management tool built for firmware 4.2 maps the response into its own model, drops the new autoWake field, and its PUT stores the device without it: nobody asked to turn it off, nothing errors. With merge patch it sends only { "display": { "brightness": 60 } } and everything else is left alone. On the server I parse that media type explicitly, return 415 with Accept-Patch for anything else, apply the patch to a copy, validate the result, strict about unknown keys, and swap it in atomically, as RFC 5789 requires."

What’s happening
  1. RFC 5789 says the server "MUST apply the entire set of changes atomically"; patch-a-copy, validate, swap gives you that for free.
  2. Express's JSON parser only parses application/json by default, so a correct merge patch arrives with req.body undefined unless you pass type: "application/merge-patch+json". The testing chapter's integration test catches exactly this bug returning 200 for a change that never happened.
  3. Merge patch semantics: a member replaces, null deletes, arrays are replaced whole, and a non-object patch replaces the whole document.
  4. If you must keep PUT, the safe options are all harder: treat missing members as unchanged, require clients to round-trip unknown members, or require If-Match plus a declared schema version.
  5. Concurrency is separate: merge patch alone doesn't stop two admins overwriting each other's array edits; If-Match does.

Learn more: Partial updates: PATCH and JSON Merge Patch.

What can't JSON Merge Patch express, and what do you do about it?
Show answer

It can't set a value to null, can't touch one element of an array, and can't say "only if the current value is X". "For most device settings that's fine. Lists that people edit one item at a time, like directory entries or provisioning servers, become collections with their own ids instead of arrays inside the settings, which removes the main reason for anything else. If I genuinely need element-level or conditional edits, I add JSON Patch, RFC 6902, application/json-patch+json, alongside it: a list of operations addressed by JSON Pointer, with a test operation as a precondition, applied all or nothing."

What’s happening
  1. In merge patch, null always means "remove this member", so nullable fields can't be set to null.
  2. Adding one NTP server means sending the whole new list built from your earlier read, which is a lost-update risk if two admins do it at once.
  3. JSON Patch's test is field-level optimistic concurrency: "remove element 0 only if it's still ntp1.example.com"; if it fails, the whole patch is discarded (RFC 6902 section 5).
  4. Pointer escaping is a classic client bug: a key like room/floor is addressed as /labels/room~1floor (~1 for /, ~0 for ~, RFC 6901).
  5. Whichever format, the server validates the merged result, not the patch: removals of required fields, a non-object patch and cross-field rules are only visible in the result.

Learn more: Partial updates: PATCH and JSON Merge Patch.

Design the API for a firmware update.
Show answer

POST /firmware-updates returns 202 Accepted at once, with a Location pointing at an operation resource that survives the reboot the update ends with. "The operation has a small status clients branch on (queued, running, succeeded, failed, cancelled), a finer step the UI shows (downloading, verifying, installing, rebooting), real progress only where something is counted, a cancellable flag, and on failure a problem-details error with a stable type like signature-invalid. Clients poll at the interval in Retry-After or listen to an event stream with polling as the fallback. A second update while one runs gets 409 with a link to the running one, and cancel is a POST to the operation that returns 409 once the flash write has started."

What’s happening
  1. RFC 9110: 202 means "accepted for processing, but the processing has not been completed", and the response should point to a status monitor. Google's AIP-151 formalises the same pattern and suggests about 10 seconds as the threshold for needing one.
  2. Separating status from step lets later firmware add steps without breaking clients that only check status.
  3. Retry-After on a running operation tells a hundred browser tabs how often polling is useful.
  4. Cancel is POST /operations/{id}/cancel, not DELETE, because you want to keep the record of a cancelled update.
  5. The UI trap: 202 is "accepted", not "installed"; the image can still fail verification minutes later, so show "Update started" and follow the operation to its final state.

Learn more: Long-running operations.

A colleague wants POST /system/reboot to block until the device is back and then return 200. What goes wrong?
Show answer

The process that would send the 200 is the one being restarted, so the answer has nowhere to go. "Its sockets close with it, the new process after boot knows nothing about that connection, and long before a minute-long reboot finishes, proxies, browsers and SDK timeouts give up anyway; a client that sees an error often retries, which is a second reboot. So the device answers 202 with an operation id, makes sure the response is written before it shuts down, keeps the operation record in flash, and the next boot marks it succeeded before the API opens. Clients treat connection refused and 503 during that window as 'in progress', not as failure."

What’s happening
  1. Send the 202 and wait for the response's finish event before starting the shutdown; otherwise the client gets a network error for a request that succeeded.
  2. The operation record lives in storage that survives the reboot; on boot, an operation started in an earlier boot is marked done. That's the only way a reboot operation can succeed.
  3. In the chapter's model the polls went: ECONNREFUSED (nothing listening), 503 Device is starting (HTTP up before the API), then 200 succeeded/done.
  4. Real clients wait up to a documented maximum (say ten minutes for an update) before declaring the device lost.
  5. Poly's public guide documents POST /rest/system/reboot as returning 200 OK with no body: the older, simpler style, where the client learns about the reboot by the device going quiet and coming back.

Learn more: Long-running operations and, for Poly's own reboot call, How a web UI, device APIs and back-end services fit together.

Two admins edit the same device settings at once. What happens, and how do you prevent it?
Show answer

Without protection the last write wins and the first admin's change silently disappears; I'd use optimistic concurrency with a strong ETag and a required If-Match. "Every GET returns an ETag; every write must send it back in If-Match. The device compares before doing anything: a match applies the change and returns the new tag; a mismatch returns 412 Precondition Failed with the current tag, and the client re-reads, re-applies its own intent and retries. I'd require the header, answering 428 Precondition Required to writes without it, so no integration can bypass the check, and give each settings section its own tag so an audio change doesn't conflict with a network change."

What’s happening
  1. The lost update happens even with merge patch, because arrays are replaced whole and both clients compute their new list from what they read.
  2. Optimistic means nobody holds a lock: a lock held by a cloud service that then loses its network would block the local admin; an ETag check costs one string comparison.
  3. In the chapter's model both admins added an NTP server; without preconditions the UI's server vanished, with If-Match the fleet service got 412, re-read "r2", retried and both servers survived.
  4. Return the new ETag on every successful write so the next change needs no GET.
  5. Status doesn't need If-Match (nobody writes it), but its ETag still makes If-None-Match polling cheap.

Learn more: Optimistic concurrency with ETags.

After you "add ETag support", every conditional write returns 412. Or worse, a stale script's write is accepted after a factory reset. What went wrong?
Show answer

Two different ETag bugs: Express's automatic tags are weak, and a revision counter that restarts on reset repeats old tags. "RFC 9110 says If-Match must use the strong comparison, and under strong comparison a weak tag like W/"1e-…" never matches anything, not even itself, so every write fails and someone eventually turns the check off. The fix is to set your own strong ETag on writable resources. The second bug is ABA: after a factory reset the counter is back at "r2" for a different document, so a script's old If-Match: "r2" wrongly matches. Derive the tag from a hash of the canonical content, or add a reset epoch so a tag never comes back."

What’s happening
  1. Express adds a weak ETag to every res.json() body by default; that's fine for If-None-Match caching, which uses weak comparison.
  2. Setting res.set("ETag", …) before res.json() makes Express keep your tag instead of generating its own.
  3. In the chapter's model the counter tag "r2" matched after a reset (wrongly accepted), while the SHA-256 content tag returned 412 correctly.
  4. Hash a canonical serialisation (sorted keys), or keep a counter outside the area a factory reset wipes, or combine it with an epoch: "e3-r2".
  5. Rule: the tag must change whenever the representation changes and never come back for a different one.

Learn more: Optimistic concurrency with ETags.

How do you design error responses that partners' code can rely on for years?
Show answer

RFC 9457 problem details everywhere, with the type URI as the contract and everything else as the message. "Every error is application/problem+json with a type that identifies the kind of problem and never changes, a title for that type, the status, a detail for this occurrence that clients must not parse, and an instance. Machine-readable specifics go in extension members, like an errors array of JSON Pointers for validation or activeCall for a busy device, and clients ignore extensions they don't know. Generic cases use about:blank. A 500 never carries a stack trace, only an instance id support can find in the device logs. The types are documented in an error catalogue, and partners switch on type, falling back to the status code."

What’s happening
  1. RFC 9457 (July 2023) obsoletes RFC 7807. title "SHOULD NOT change from occurrence to occurrence"; status is only advisory and must match the real one; clients "MUST ignore any such extensions that they don't recognize".
  2. Types are forever: never rename or reuse one, use absolute URIs on a domain you'll keep, and localise title and detail, never type.
  3. If useful information only lives in detail, someone will regex it, and your next copy-editing pass breaks their integration; put every value a program needs in an extension.
  4. A good client also copes with errors that aren't problem details at all, like a proxy's HTML 502, by falling back to the status.
  5. Extending an existing API: Poly's public guide uses 415 for "Payload invalid or missing one or more required fields" and 404 for "Operation or source unavailable". Partners code against what a device actually returns, so add problem bodies without changing existing status codes, and change codes only in a new API version.

Learn more: Errors with problem details.

An admin applies a 200-setting configuration profile, and setting 37 is invalid. What should happen?
Show answer

Nothing at all: one request is one transaction. "The device checks every value, then runs cross-setting rules on the complete candidate configuration, such as 'SIP mode needs a SIP server', and only if everything passes does it swap the configuration in one step and bump the revision. Any error returns 422 with every problem and applied: false. I'd offer a dry run that runs the same code minus the final swap and returns a diff with each change marked live or restart-required, require If-Match with the configuration revision so a stale profile can't undo today's edits, and report restart-required settings as pending in /status instead of pretending they took effect."

What’s happening
  1. Applied one PATCH at a time, a group passes through states nobody designed (SIP mode with no SIP server yet), and a failure halfway leaves the device half-configured with no record.
  2. The dry run must be the same code path as the real apply; a separate validator drifts. Kubernetes' server-side ?dryRun=All is the same idea.
  3. Return a diff, not just "valid": provider.mode: teams → sip (restart) is what reviewers and fleet tools need.
  4. Network equipment has worked this way for years: NETCONF (RFC 6241) edits a candidate configuration, validates it and commits.
  5. Applying a profile to 500 rooms is a different, cloud-side job with partial success per device; on each device keep it all or nothing.

Learn more: Bulk and transactional configuration.

An admin moves a remote device to the wrong VLAN and can no longer reach it. How should the design have prevented a site visit?
Show answer

Confirm or revert: settings that can cut the admin off are applied on probation, and the device itself rolls them back unless someone confirms from the new state. "That's NETCONF's confirmed commit: RFC 6241 says such a commit must be reverted if no confirming commit arrives within the timeout, 600 seconds by default, or if the device reboots first. So POST /config/changes with a confirm timeout arms a timer on the device, the UI polls the new address, and only a successful confirmation from there keeps the change. The timer has to live on the device; a timer in the browser is useless once the browser can't reach it."

What’s happening
  1. Validation proves an IP address is well-formed, not that the admin can still reach the device on it: wrong gateway, wrong VLAN, a certificate the browser won't accept, an HTTP port turned off.
  2. In the chapter's model the mistaken VLAN change rolled back by itself at 60 s and the admin could reach the device again; the planned move was confirmed from the new VLAN and kept.
  3. The confirm request is cross-origin: the UI was loaded from the old address, so the session cookie isn't sent to the new one. Return a one-time confirm token and allow exactly the old origin for that endpoint, or move the whole page to the new address.
  4. Apply it to network, ports, certificates and auth changes only; for everything else the extra minute of uncertainty isn't worth it.
  5. With it, a timed-out save of a static IP has an outcome that can't stay "unknown" for long.

Learn more: Bulk and transactional configuration and Apply, restart and rollback flows.

An admin dashboard polls /status (2 KB) and /settings (8 KB) every 5 seconds, and five people leave it open. What does that cost the device, and what are the two biggest fixes?
Show answer

About 36,000 KB of response bodies an hour and 7,200 serialisations, 80% of it settings that change twice a day. "Fix one: a strong ETag and If-None-Match on settings, with freshness checked before the document is built, so nearly every poll is a body-less 304 the device doesn't even render. Fix two: push status as events on one stream per tab, carrying only the fields that changed. Under the assumptions below, that's about 900 KB an hour instead of 36,000."

rq-polling-cost.mjs
// An admin dashboard polls /status (2 KB) and /settings (8 KB) every 5 s; five tabs stay open all hour.
// Assumptions (stated, not measured): settings change twice a day; status changes on most polls, and an
// event carrying only the changed fields is about 0.25 KB. Response bodies only; headers left out.
const tabs = 5, intervalS = 5, statusKB = 2, settingsKB = 8, eventKB = 0.25, settingsChangesPerDay = 2;
const pollsPerTab = 3600 / intervalS;                       // polls of each resource, per tab, per hour
const kb = (n) => `${n.toLocaleString("en-US", { maximumFractionDigits: 1 })} KB`;

const plainSettings = tabs * pollsPerTab * settingsKB;
const plainStatus = tabs * pollsPerTab * statusKB;
console.log(`plain polling: settings ${kb(plainSettings)} + status ${kb(plainStatus)} = ${kb(plainSettings + plainStatus)}/hour`);
console.log(`  requests the device serialises: ${(2 * tabs * pollsPerTab).toLocaleString("en-US")}/hour`);

// Fix 1: strong ETag + If-None-Match, freshness checked before rendering: a body only after a change.
const fix1Settings = tabs * (settingsChangesPerDay / 24) * settingsKB;
console.log(`fix 1, settings answered 304 unless changed: ${kb(fix1Settings)}/hour`);
// Fix 2: status pushed as one event stream per tab, only the fields that changed.
const fix2Status = tabs * pollsPerTab * eventKB;
console.log(`fix 2, status as events: ${kb(fix2Status)}/hour`);
const after = fix1Settings + fix2Status;
console.log(`after both: ${kb(after)}/hour, ${(100 * (1 - after / (plainSettings + plainStatus))).toFixed(1)}% less`);
Result
plain polling: settings 28,800 KB + status 7,200 KB = 36,000 KB/hour
  requests the device serialises: 7,200/hour
fix 1, settings answered 304 unless changed: 3.3 KB/hour
fix 2, status as events: 900 KB/hour
after both: 903.3 KB/hour, 97.5% less
What’s happening
  1. Each tab polls each resource 3600 / 5 = 720 times an hour, so five tabs make 3,600 polls of each: 3600 × 8 KB = 28,800 KB of settings and 3600 × 2 KB = 7,200 KB of status, the same numbers as the chapter's quiz.
  2. With conditional requests, a settings body is only sent after one of the two daily changes: 5 × (2/24) × 8 KB ≈ 3.3 KB an hour. The 304s still cost headers and a request each, which this model leaves out; the chapter's model shows why you check req.fresh before rendering, so the device doesn't serialise 30 documents to send 2.
  3. Events replace full 2 KB documents with small deltas: at an assumed 0.25 KB per change, 3600 × 0.25 KB = 900 KB. The event size and change rate are assumptions; change them to your device's.
  4. A trap that silently cancels fix one: setting If-None-Match yourself on fetch with the default cache mode makes the Fetch standard add Cache-Control: no-cache, and Express then always sends a full 200. Use cache: "no-cache" on hand-written conditional requests.
  5. Then lengthen the interval for anything not on screen; a hidden tab can poll once a minute or stop.

Learn more: Designing for constrained devices and Polling, long polling, SSE and WebSockets for device status.

What changes when the API server is the device itself rather than a cloud service?
Show answer

It can't scale out, and it shares the CPU with the device's real job, video and audio, so the API has to be cheap by design and bounded everywhere. "Polling is made cheap with strong ETags and freshness checked before rendering; live status is pushed as events rather than polled; responses are small, with settings in sections, ?fields=, paginated lists with a hard maximum and large files streamed. Every request is bounded: per-route body limits that return 413, per-client rate limits that return 429 with Retry-After, one expensive operation at a time, a connection cap and short timeouts. And the device tells well-behaved clients how to back off, with Retry-After while booting and on running operations. Then measure on the real hardware."

What’s happening
  1. Reject by rate before parsing the body, so a flood of large requests costs almost nothing.
  2. Express's JSON parser defaults to 100kb; set limits per route, and give firmware uploads their own streaming endpoint. Poly's public REST guide documents a 10 MB cap for its API.
  3. Node 26's HTTP server defaults include a 5-minute requestTimeout and no connection cap; a device wants shorter timeouts on ordinary routes.
  4. A raw server.maxConnections cap refuses clients with a reset, not a helpful 503, and can lock out the admin with the dashboard; combine a generous cap with limits on expensive operations.
  5. Compression costs CPU: often better to pre-compress large cacheable downloads and leave small API responses alone. Measure on the device.

Learn more: Designing for constrained devices.

Trace one "Save" click from the admin web UI to the hardware and back.
Show answer

Browser, device web server, auth and validation, settings service, platform service, and back, with nothing reporting "done" before the platform confirms. "The browser sends a PATCH with only the changed fields; the device's own web server terminates TLS; the API layer checks the session cookie and XSRF header and validates the body against the schema, so bad requests stop before anything changes. The settings service stages the change and calls the platform service that owns the resource, on Android usually a system service over Binder, and waits. On success it commits a new config version, publishes a settings-changed event to every subscriber, and the API returns 200 with the version and a restart-required flag. On failure it rolls back, and the API maps the platform's error to a stable one, like 503 with Retry-After for 'busy'."

What’s happening
  1. Public Poly documents don't describe their devices' internals, so this is the general design, not PolyOS.
  2. The slow, failure-prone part is almost always the platform call, not the web stack (120 of the model's 129 fake milliseconds).
  3. The ordering bug to avoid: bumping the version and sending the event before the platform call, so a failed save announces "Wi-Fi on" to every screen while the radio stays off.
  4. Error codes are translated in one place, the API layer, so a platform refactor can't change what partners see.
  5. Slow operations (reboots, updates, log bundles) return 202 with an operation instead of holding the request open.

Learn more: How a web UI, device APIs and back-end services fit together and Anatomy of a managed device.

An admin sets a 300-character device name through the CLI. The web UI header breaks and the cloud rejects the heartbeat. Where does the fix go?
Show answer

In the settings layer's schema, so every writer is refused with the same error. "The value got in through the CLI, but patching the CLI parser leaves REST and provisioning open, and truncating the header in the UI hides the symptom for one viewer. I'd add maxLength to device.local.deviceName in the one schema every front door calls, so CLI, REST and provisioning all get INVALID_VALUE, and the UI reads the same limit before the user presses Save. Then the device-specific part: devices in the field may already hold a long name, so the release needs a migration that meets that old data on first boot."

What’s happening
  1. The architecture point: one schema and one command layer behind every front door, because separate validators per door drift apart over years of releases.
  2. In the chapter's model, the CLI write and the REST read went through the same write function and landed in the same layer: two doors, one truth.
  3. The config store has layers (defaults, local, provisioned that win), and each read reports its source, so the UI can say "managed centrally".
  4. Fixing only the UI is the "two front doors, two validators" bug in reverse.
  5. A new rule always meets old data on devices: plan the truncate-or-flag migration in the same change.

Learn more: Anatomy of a managed device and Validation shared between UI and device.

What is OpenAPI 3.1, and what changed from 3.0 that bites in practice?
Show answer

A machine-readable description of an HTTP API, and in 3.1 its schemas are real JSON Schema 2020-12. "Paths, operations, parameters, request and response bodies, security schemes, and in 3.1 a root webhooks map for requests the device sends out. Because the file is data, it generates docs and types, validates traffic at runtime and gets diffed for breaking changes. The changes that bite: nullable is gone in favour of type: [string, "null"], exclusiveMinimum is a number instead of a boolean, schema example became examples arrays. And openapi: 3.1.1 is the spec version, info.version is your API's version; mixing them up is a classic slip."

What’s happening
  1. Dates from the specification's revision history (checked 27 October 2026): 3.0.0 July 2017, 3.1.0 February 2021, 3.2.0 September 2025 with a 3.2.1 patch in September 2026.
  2. Tools disagree about 3.0 habits in a 3.1 file: in the chapter's model Ajv and openapi-typescript both accepted nullable: true, while Redocly's linter rejected it. A strict validator in another language might reject every null.
  3. A 3.0 { minimum: 0, exclusiveMinimum: true } makes Ajv in 2020-12 mode refuse to compile the schema: "data/exclusiveMinimum must be number". Good: a loud failure at startup.
  4. Why 3.1 and not 3.2 today: swagger-parser 13.1 rejects 3.2 documents, so move when the validator, type generator and docs tool all support it.
  5. A file starting swagger: "2.0" is the older format; convert it once rather than maintaining both.

Learn more: OpenAPI 3.1 in an interview.

How do you write the OpenAPI spec for a device's local API, and how do you know it's any good?
Show answer

Shared components for every shape, every operation with its error responses, and CI that checks the file in three ways. "One schema per resource, one Problem schema for every error as application/problem+json, reusable responses and the If-Match header defined once. Every operation gets a unique operationId, a summary, tags and its 4XX responses; partial updates are application/merge-patch+json with a patch schema that has additionalProperties: false; reboots are 202 plus an operation; the server URL is a variable because every device is its own host. In CI, swagger-parser proves it resolves, a linter like Redocly or Spectral proves it makes sense, and a script validates every example against its own schema, because examples are what partners copy."

What’s happening
  1. swagger-parser and a linter catch different things: in the chapter's model a broken $ref failed both, but a duplicate operationId, an undeclared path parameter and type: integr passed swagger-parser and only Redocly caught them.
  2. Validators are software with versions: swagger-parser 13.1 rejected a server-variable description because its bundled schema iteration misspelled the property. Pin tools and know their bugs.
  3. Examples drift: copiedFromOldGuide failed with "62" where an integer was required.
  4. Writing a spec from a PDF reference guide surfaces its gaps: the linter's security-defined error showed that Poly's public guide never says how the session id travels on later requests.
  5. Name schemas and operationIds for the long run: they become type and method names in generated code, so renaming them breaks SDK users even when the wire format is identical.

Learn more: Writing a spec for a device API.

How do you keep the running API and the spec from drifting apart?
Show answer

Make the spec executable: compile its schemas at startup and check every request before the handler and every response after it. "Middleware validates the media type, parameters and body against the operation in the spec and turns failures into problem details with JSON Pointers to the bad fields: 415 for a media type the spec doesn't declare, 400 only for a body that can't be parsed, 422 for well-formed JSON that breaks the schema, like volume 150. Handlers only ever see valid data. Responses are validated against the schema for their status code: in tests and staging a violation fails the request, so the first test that hits a contract bug says why; in production I'd log violations, or sample, rather than turn a shipped contract bug into an outage. Compile once, validate many."

What’s happening
  1. In the chapter's model a native layer returned muted: 0; the response validator caught "data/muted must be boolean" before a body that would break every typed client went out.
  2. A valid body sent as application/json to an operation that only declares application/merge-patch+json gets 415: the media type is how the client says which semantics it means. Fix it in the client or the spec, never with an undeclared exception in code.
  3. Query and path parameters arrive as strings, so they need coercion; in Express 5 req.query is a getter that re-parses the URL on every read and can't be assigned, so copy the coerced values (to res.locals) instead of mutating it.
  4. format: date-time isn't checked unless you add formats; Ajv's strict mode refuses to compile an unknown format rather than pretend.
  5. coerceTypes, removeAdditional and useDefaults mutate data: fine for parameters, but on bodies they hide client bugs a stricter device would reject.

Learn more: Validating requests and responses against the spec.

What do you generate from the spec, and what don't you trust the generated types for?
Show answer

Types, a thin client, docs and mocks, but never values: types check shapes, the device still validates. "openapi-typescript turns the spec into TypeScript types, so a wrong field name, a string where a number belongs or an enum value that doesn't exist is a compile error in the web UI. A thin typed client like openapi-fetch sits on top, or a generated SDK for partners in other languages; Redoc or Swagger UI render the docs and a mock server serves the examples so the UI team can start early. What the types lose: integer becomes number, and minimum, maximum, pattern, minProperties and additionalProperties vanish, so { volume: 150 } compiles and the device rejects it at runtime."

What’s happening
  1. In the chapter's type-check model, volume: "70" (TS2322), speaker: "BOTH" (TS2322) and balance: 0 (TS2353) failed to compile; {} and { volume: 150 } compiled.
  2. The excess-property check only applies to object literals: a patch built elsewhere with extra fields compiles, and the device rejects it.
  3. Generated files are never edited by hand: CI regenerates them and fails on a git diff.
  4. Docs belong on a developer portal, not on a constrained device; the device serves its spec as JSON for discovery.
  5. Commit generated SDKs partners download (they need a changelog); regenerate types used only inside one repo in the build.

Learn more: Generating types, clients and docs.

Design-first or code-first, for a team split between the US and India?
Show answer

Design-first for anything partners and other teams consume, code-first for internal endpoints that ship in lockstep with their only consumer. "The spec pull request is an asynchronous design review: a developer in India proposes an endpoint as a YAML change at the end of their day, I review the shape while they sleep, and the UI team starts against a mock the next morning, with no meeting. Its risk is drift, so the spec is enforced: runtime validation, examples validated in CI, a breaking-change diff on every PR. Code-first with something like Zod is fine for internal endpoints, as long as the generated spec is reviewed and diffed too, because its defaults can quietly turn into partner bugs."

What’s happening
  1. Design-first fails by drift unless enforced; code-first fails by accident, when a refactor changes a public contract nobody knew was public.
  2. The chapter's Zod model found two real differences: z.toJSONSchema emitted additionalProperties: false on the response schema (a strict client then breaks the day the device adds a field), and a .refine for "at least one field" vanished.
  3. The spec PR is the cheapest place to change your mind: a rename costs one comment there and a deprecation cycle after the UI, device and a partner shipped against it.
  4. A common hybrid: design-first for the public surface, code-first internally, both through the same lint and diff steps.
  5. A review checklist keeps async review consistent: naming, declared errors, patch semantics, pagination, limits, compatibility with older firmware.

Learn more: Design-first vs code-first.

The device API is at 1.4.0. Which of these can ship in 1.5.0: rename muted to isMuted, make firmwareVersion optional in a response, narrow volume to 0–80 in a request, add an optional ?since= query parameter?
Show answer

Only the optional query parameter. "The rename is a removal plus an addition, and removing a field clients read is breaking; the safe way is to add isMuted, keep muted, and deprecate it. Making a response field optional is breaking, because clients that assumed it's always there now need a null check. Narrowing a request range breaks clients that sent 90 yesterday. A new optional query parameter is additive as long as the behaviour without it is unchanged. Direction decides most verdicts: what's safe in a response can break in a request, and the other way round."

What’s happening
  1. Request direction: adding a required field, making an optional one required, narrowing a range or removing an enum value is breaking; widening is safe.
  2. Response direction: removing a field, changing its type, or making a required field optional is breaking; adding a field is safe for tolerant readers; adding an enum value is risky for clients with exhaustive switches.
  3. No diff tool can tell a rename from a removal, which is why renames are always add-and-deprecate.
  4. Changing a parameter's default changes no schema shape, so naive diffs miss it, yet every client that relied on the default sees different behaviour.
  5. A schema diff can't see behaviour changes (a reboot that now waits for the call to end), so "0 breaking changes" is necessary, not sufficient.

Learn more: Contract testing and Rules for compatible change.

A spec diff says a change is breaking. How do you find out whom it actually breaks?
Show answer

Consumer-driven contracts: each consumer records the calls and fields it really uses, and the provider replays every consumer's list against the new build before it ships. "In the chapter's model, renaming muted to isMuted broke the web admin UI's two interactions, while the partner room panel and the fleet manager passed, because they never read that field. That turns a yes/no gate into a decision with names attached: the web UI ships in the same firmware image and can change in the same release; had it broken the partner panel, it would need a deprecation period and a conversation. Pact automates this; for partners who won't run your tooling, ask for the endpoints and fields they use and keep that as a contract file."

What’s happening
  1. Provider conformance alone can't catch it: the 2.0.0 build matched its own new spec perfectly.
  2. Keep the released spec of every supported firmware line in the repo and diff against each, because a change additive to 5.0 can still break an integration written for 4.6.
  3. oasdiff's documented levels are ERR for definite breaks and WARN for ones that can't be confirmed programmatically, with --fail-on for CI.
  4. A field that's in the spec but in no consumer contract is a candidate for deprecation, never for silent removal: partners who don't publish contracts may use it.
  5. Integrator-facing release notes cover the behaviour changes neither diff can see.

Learn more: Contract testing.

What makes a good admin web UI for a hardware device?
Show answer

It tells the truth about the device's state, makes changes safe, and keeps working while the device is only half alive. "I'd start from the operator's five jobs: is it healthy, change settings, diagnose, update, secure it, which gives a dashboard, settings, diagnostics, maintenance and security. The dashboard is computed from the device's status into a short list of problems with actions, worst first, not a wall of numbers. Wherever they can differ, it shows what's saved, what's active, and when it last heard from the device. And the UI is the API's first client: everything it does goes through the same public REST API scripts and partners use, so a setting only the UI can change is a setting nobody can automate."

What’s happening
  1. The mental model is a cockpit for a remote aircraft: the admin usually isn't in the room, people may be in a meeting there right now, and a wrong change can take the room offline with no way back in.
  2. In the chapter's model, a disconnected ceiling mic became the one critical item, a pending restart and an expiring certificate warnings, and an available update only "info": showing "available" in red trains admins to ignore red.
  3. Because the dashboard is a function of state, the same function can run over a thousand devices in a fleet view, and "a disconnected mic is critical" is a unit test.
  4. The most common failure is a UI that shows what the admin asked for instead of what the device is doing: "Teams" on the page while the old app still runs until a restart.
  5. Poly's public 4.7.0 guide says the web interface can configure every setting while the in-room interface is intended for initial setup, and that provisioned settings become read-only there.

Learn more: What device admin UIs must do well.

The device has hundreds of settings, and they differ by model and firmware. How do you build the settings pages?
Show answer

Describe each setting once, as data, in a JSON Schema served by the device, and generate the form, both validators, the docs and the SDK types from it. "JSON Schema 2020-12 already has type, enum, ranges, patterns, defaults, titles, readOnly, writeOnly and if/then; I add annotation keywords for device facts it lacks, like x-restart and x-group. A generic renderer in React or Angular turns the generated field list into accessible fieldsets: the widget comes from the type, visibility from if/then, constraints become input hints. Because each device serves its own schema, a UI talking to old firmware builds exactly the form that firmware supports."

What’s happening
  1. Four hand-written copies (form, browser validator, device validator, docs) drift; one blueprint, rebuilt, doesn't.
  2. The if/then rule "static IP needs an address and a prefix" drives both validation and showWhen, so the UI can never hide a field the device requires.
  3. A writeOnly password becomes a password field and is left out of exports: one annotation, two behaviours.
  4. Libraries exist (react-jsonschema-form, JSON Forms, ngx-formly), good for internal tools; your own renderer fits a strong design system and device-specific widgets. Either way the schema stays the source of truth.
  5. Decide early where presentation lives: meaning (type, limits, restart) in the settings schema partners read, order and help text in a separate UI schema.

Learn more: Forms generated from a settings schema.

Where does validation live in your design, and why does the browser use different Ajv options from the device?
Show answer

One schema, compiled in two places: in the browser for instant feedback, on the device as the authority, plus device-only checks the UI can't make. "The browser compiles with allErrors so the form can mark every bad field at once. The device compiles stopping at the first error, with a body-size limit, because its API is reachable by scripts that never saw the form, and Ajv's own security docs say 'Do NOT use allErrors in production'. Rules only the device knows, like the connected display's maximum resolution, come back as 422 problem details with a JSON Pointer, so the UI puts the message on the right field. And the device never trusts that the UI ran."

What’s happening
  1. Three layers: field rules and cross-field rules live in the schema and run on both sides; device-state rules live in device code.
  2. Ajv 8 is strict: an unknown keyword like x-group throws until you register it with addVocabulary. Don't turn strict mode off.
  3. In the chapter's model, firmware 4.2.1's schema had no 4K, so the UI blocked it before sending; a script skipping the UI got the same 422 from the device.
  4. Build user messages from Ajv's keyword and params, not its English message, so they can be translated and suggest a fix (WCAG 3.3.3).
  5. Under a strict CSP Ajv can't compile at runtime (it uses new Function), so compile standalone code at build time, bundle it, and import the bundle in CI.

Learn more: Validation shared between UI and device.

At 9:58 an admin changes the conferencing provider, which needs a restart, and the room is in a call until 10:30. What does Save do?
Show answer

Save the setting, don't restart, and say so. "'Saved. The new provider starts after a restart. This room is in a call: restart now, which ends the call, or later?' The batch goes to the device as one merge patch with If-Match; the device stores it, applies live settings at once and returns which settings are pending a restart. That list is device state, so the dashboard keeps showing 'Restart needed to apply: provider' to anyone who looks until it happens. Silently restarting would drop a live meeting; silently not restarting leaves the admin believing Teams is active."

What’s happening
  1. The restart flag comes from the schema (x-restart), so the Save button becomes "Save and restart…" only when the pending batch includes one.
  2. Edits changed back to their loaded value are dropped from the pending list, so the button doesn't claim a restart that isn't needed.
  3. "Later" can be a maintenance window; Poly's 4.7.0 guide documents a scheduled daily or weekly auto restart.
  4. On a 412 because another admin saved first, rebase the pending edits three ways (baseline, mine, current) and show only real conflicts.
  5. Collect, then restart once: five restart-required changes cost one restart.

Learn more: Apply, restart and rollback flows.

The upload bar reaches 100%. A developer wants to show "Update complete". What's wrong?
Show answer

Nothing has been installed yet, and the hardest steps haven't started. "After the upload the package still has to pass the checksum and the signature check, then install to the inactive slot, reboot, and pass a post-boot health check before the device marks the new version good. In the chapter's model, five of six scenarios ended badly after a 100% upload: two rejected, two rolled back, one not responding. So the UI shows each phase honestly: real percentages where something is counted, an indeterminate state during the reboot, and 'complete' only when the device reports it's running the new version after its check."

What’s happening
  1. The checksum catches accidents (a zeroed chunk); the signature against the vendor's public key catches forgeries with a matching self-computed checksum.
  2. On Android devices built with A/B updates, update_engine writes the unused slot and the new slot only counts as good once markBootSuccessful() is called; the public Poly docs don't say which scheme PolyOS uses.
  3. "Never comes back" after the deadline is "not responding, check the room", never "failed": the UI only knows it can't reach the device.
  4. Uploads are resumable: the device says how much it has (Upload-Offset, from the IETF resumable-upload draft, still not an RFC) and the progress bar shows the device's count, not bytes sent.
  5. Once uploaded, the device owns the rest; the UI is a window onto an operation resource, so closing the laptop doesn't break the update.

Learn more: Software update flows.

The device stops answering in the middle of an admin's session. What should the UI do?
Show answer

Know which kind of silence it is, keep showing what it last knew, and never queue writes. "One failed poll is noise: keep the data, marked 'as of' the last success. A few in a row is unreachable: a status banner says so, writes are disabled with one disabled fieldset, unsaved edits are kept, and polling backs off to a cap. If the admin started a restart, silence is expected, so it says 'Restarting…', polls steadily and only escalates after a deadline based on the measured boot time. Every status reply carries a boot id: the same id means a network blip, a new one means the device restarted, so reload settings and version and expect to log in again."

What’s happening
  1. Don't queue writes for later: an admin who pressed Save and left didn't agree to it being applied an hour later in a different context.
  2. Don't retry a timed-out write blindly: re-read first, because you can't know whether it was applied (a static IP change may have succeeded and moved the device).
  3. Red "unreachable" during a restart the admin asked for is a success that looks like a failure, and it trains admins to ignore the real one.
  4. A DHCP device can come back on a different address; the "lost" screen shows hostname and MAC, and fleet tools avoid the problem because devices connect out to them.
  5. navigator.onLine is about the admin's machine, never about the device.

Learn more: Device unreachable, rebooting and offline states and Reconnection and back-off.

How would you design configuration import and export for fifty rooms?
Show answer

Export a versioned file that leaves secrets and per-device identity behind; import it through a pipeline that migrates, filters, validates and shows a dry-run diff before one atomic apply. "The export records its format, schema version, source model and firmware, and lists what it omitted: write-only secrets like the admin password, and per-device identity like the room name and IP, which would give fifty rooms the same address. Import migrates old files forward one version at a time, skips what the target model can't take with a reason, validates with the same schema as the forms, and shows only real changes. With a provisioning service, every setting has a source, and the device refuses local writes to provisioned ones with 409."

What’s happening
  1. An export file is a secret until proven otherwise: support tickets and shared folders leak them. A "full backup with secrets" is a separate, named, encrypted, audited action.
  2. Migrations are small pure functions, one per schema step, kept forever, so a 2024 export still imports.
  3. The dry run is the end state until the admin approves; the apply is the normal merge patch with If-Match and restart handling.
  4. Poly's public 4.7.0 guide: "Provisioned settings are read-only in the system web interface", and a system that loses registration keeps the most recent configuration it received. A provisioning template can carry the admin password, so treat it like a secret too.
  5. The device, not just the greyed-out control, refuses the write, so a script gets the same answer as the form.

Learn more: Import, export and provisioning of configuration.

What do you check to make a device admin UI accessible and ready for other languages?
Show answer

WCAG 2.2 AA done consistently, and Intl plus translator-owned templates for everything locale-dependent. "Real labels on every field; errors as text on the field, linked with aria-describedby, with a suggested fix; nothing conveyed by colour alone; status changes like 'Saved', 'Restarting' and 'Device not responding' in a role="status" live region; visible focus that a sticky bar never covers; targets of at least 24 pixels; a login that works with password managers and paste; session timeouts that warn and can be extended. For localisation: dates, numbers, units, relative times and plurals through Intl, device times shown with the zone labelled, and the device returning error codes and parameters, not English sentences."

What’s happening
  1. WCAG 2.2 is a W3C Recommendation (republished 12 December 2024); 2.4.11 Focus Not Obscured, 2.5.8 Target Size and 3.3.8 Accessible Authentication are new in 2.2.
  2. Show both clocks: the room's (what people there remember) and the admin's (their calendar), with the zone in the string.
  3. Plurals differ by language: Polish needs one, few and many, which Intl.PluralRules handles and n === 1 ? … : … doesn't.
  4. Use logical CSS properties for right-to-left, and dir="ltr" on IP addresses, versions and log lines even inside Arabic text.
  5. Test with pseudo-localised strings; short English labels can grow by 200–300% in translation.

Learn more: Accessibility and localisation for admin UIs.

#Telnet, SSH and real-time questions

Added

The same deck, for the channels that aren't request and response: command lines over Telnet and SSH, live device state pushed to browsers and control systems, and the calls themselves. Answer out loud first, then compare: did you say what you'd build, the mechanism that makes it work (a delimiter, a sequence number, a host key, a candidate pair), and what breaks without it?

The mental model for this group is a radio channel shared between a conversation and announcements: the hard part is never sending bytes, it's knowing where one message ends, which line answers which question, what you missed while you were away, and whether the other end is still there.

This group covers Telnet's wire format and why devices keep it, line-based servers in Node, command grammars, notifications on a command channel, SSH, one command layer behind every door, testing and securing command ports, then real-time device state (transports, events, snapshot plus events, reconnection, fan-out) and the basics of video calls: SIP and SDP, WebRTC and ICE, and media quality.

What is Telnet, and why would a 2026 video device still expose a Telnet API?
Show answer

A plain-text protocol over TCP (RFC 854, 1983) with in-band option negotiation, kept because room control systems and integrators' scripts speak short text lines over TCP or RS-232. "Byte 255, IAC, means 'the next bytes are a command': WILL, WONT, DO, DONT negotiate options such as echo or window size, and either side can always refuse, which is why it interoperates with anything. A control panel on the wall that starts a call or mutes the room can drive it with no HTTP stack and no JSON parser, and those integrations are expected to survive hardware generations. Poly's public guides describe exactly this: a command-line API on Telnet port 24 by default or 23, and SSH on port 22 'if you want encrypted access', both off by default. The catch is that Telnet has no encryption, so I'd keep it off by default, local-network only when on, and steer integrators to SSH or REST over TLS."

What’s happening
  1. Both ends start as a Network Virtual Terminal: 7-bit ASCII, lines ending in CR LF; anything fancier is an option. Saying no is always safe because the plain NVT is the fallback.
  2. Poly's Video Mode Administrator Guide documents the settings: "Enable Telnet Access", "Enable Legacy API Over SSH"; sec.telnet.enable and sec.ssh.enable default to false in the parameter guide.
  3. Telnet asks for a password by default, but the guide says it can't be locked for too many failed logins the way web and SSH access can; a session "disconnects after three failed login attempts".
  4. On a G7500 the same API is reachable over RS-232, whose device.serial.mode defaults to "Control"; the Studio G62 and the X-series bars have no serial port.
  5. Poly also publishes a REST API for the same systems, so the CLI sits next to REST, not instead of it. How they're built inside the device isn't in the public docs.

Learn more: What Telnet is and why devices still expose it and Poly video devices and PolyOS in brief.

A line-mode command server, not echoing, receives FF FD 01 and then FF FE 01. What does it send back for each?
Show answer

FF FC 01 for the first, and nothing for the second. "FF FD 01 is IAC DO ECHO: the client asks the server to echo. A line-mode API doesn't want to, so it refuses with IAC WONT ECHO, FF FC 01: RFC 854 requires an answer to a request for a change of mode. FF FE 01 is IAC DONT ECHO, a request for the mode the server is already in, and RFC 854's loop rule says you don't acknowledge that, or the two sides can bounce replies forever."

What’s happening
  1. The verbs: WILL FB, WONT FC, DO FD, DONT FE; options are one byte (01 ECHO, 03 SUPPRESS-GO-AHEAD, 1F NAWS).
  2. A refusal of DO is WONT with the same option byte; a refusal of WILL is DONT.
  3. The loop rule has three parts: only request a change of state, never announce the state you're in; don't reply to a request for the current mode; don't repeat a rejected request until something changes.
  4. Interactive login servers do the opposite (WILL ECHO, so the client stops echoing and a password prompt hides typing); a command API wants whole lines and never echoes.
  5. Inside data, a literal byte 255 is sent as FF FF, so the parser must unescape it.

Learn more: What Telnet is and why devices still expose it.

A real Telnet client's first command fails with garbage like \xff\xfd\x03volume get, while nc works fine. Why?
Show answer

The server is a raw TCP line server, and a real Telnet client opens with option negotiation bytes that end up in the command parser. "Many device 'Telnet APIs' are really plain text over TCP, and integrators who use nc or a control system's TCP socket never send an IAC byte, so it works for them. A real Telnet client may start with a burst of IAC DO … and IAC WILL …. The fix is a byte-level Telnet parser in front of the line framer that strips and answers IAC sequences, even if you never intend to support a single option, so the command layer never sees a 255 byte."

What’s happening
  1. The layers stack in a fixed order: socket bytes → Telnet parser (strip IAC, answer options, unescape FF FF, drop the NUL after a bare CR) → line framer → command layer.
  2. Frame first and you can split a line in the middle of a subnegotiation that happens to contain 0A.
  3. The parser must be a byte-at-a-time state machine: half a command at the end of one chunk is normal TCP behaviour, so chunk.indexOf(255) breaks.
  4. Accept both CR LF and bare LF as end of line: clients and scripts differ.
  5. A sensible policy for a command API: accept or refuse window size, refuse DO ECHO and terminal type, stay line by line.

Learn more: What Telnet is and why devices still expose it and Building a line-based command server in Node.

A command arrives in three TCP chunks: "vol", "ume set 42\r\nmute g", "et\r\n". What does a server that treats each data event as commands do, and what should it do?
Show answer

The naive server runs four broken commands; a framing server runs exactly volume set 42 and mute get. "TCP is a byte stream, not a message stream: nothing preserves the boundaries of the client's writes, and on localhost you almost always see one event per write, which is why this bug ships and then appears on congested Wi-Fi. So each session keeps a buffer, appends every chunk, runs a command for every newline it finds, and keeps the remainder for the next chunk, with a cap so a client that never sends a newline can't exhaust memory."

rq-framing.mjs
// TCP delivers bytes, not messages. The same three chunks, read two ways.
const chunks = ["vol", "ume set 42\r\nmute g", "et\r\n"].map((s) => Buffer.from(s));

// Naive: treat every data event as whole commands.
const naive = [];
for (const c of chunks) naive.push(...c.toString().split("\r\n").filter(Boolean));
console.log("naive :", JSON.stringify(naive));

// Framed: keep a per-session buffer, cut at LF, keep the remainder for the next chunk.
let pending = Buffer.alloc(0);
const framed = [];
for (const c of chunks) {
  pending = Buffer.concat([pending, c]);
  let lf;
  while ((lf = pending.indexOf(0x0a)) !== -1) {
    framed.push(pending.subarray(0, lf).toString("utf8").replace(/\r$/, ""));
    pending = pending.subarray(lf + 1);
  }
  console.log(`after chunk ${JSON.stringify(c.toString())}: commands ${JSON.stringify(framed)}, buffered ${JSON.stringify(pending.toString())}`);
}
Result
naive : ["vol","ume set 42","mute g","et"]
after chunk "vol": commands [], buffered "vol"
after chunk "ume set 42\r\nmute g": commands ["volume set 42"], buffered "mute g"
after chunk "et\r\n": commands ["volume set 42","mute get"], buffered ""
What’s happening
  1. The naive loop splits each chunk on \r\n and runs every piece: vol, ume set 42, mute g and et, four unknown commands from two real ones.
  2. The framed loop appends "vol" to the buffer and finds no LF, so it runs nothing and keeps "vol".
  3. The second chunk completes volume set 42\r\n: the line is cut at the LF, the trailing \r is stripped (so bare-LF clients like nc work too), and "mute g" stays buffered.
  4. The third chunk completes mute get. Decoding happens on a complete line's Buffer, never per chunk, so a two-byte é split across chunks isn't turned into �� (or use StringDecoder, which holds the dangling byte back).
  5. The real server adds what this model leaves out: a maximum line length that answers error: line too long and discards up to the next LF, per-session state in the connection closure, and an error handler on every socket.

Learn more: Building a line-based command server in Node.

What limits does a device's command server need, and what does each one do when it's hit?
Show answer

A maximum line length, a maximum number of sessions, an idle timeout, an output cap for slow readers, and an error handler on every socket. "A line over the limit gets one clear error and the rest of it is skipped up to the next newline, so its tail isn't parsed as a new command. server.maxConnections refuses connections beyond the limit before any per-session memory is allocated; on a device I'd rather accept, write error: E_BUSY too many sessions and close, so the integrator sees why. socket.setTimeout only emits timeout; Node doesn't close the socket for you, so the handler writes a reason and ends it. And socket.on('error') means one client resetting its connection can't crash the server."

What’s happening
  1. In the chapter's model, the third connection over maxConnections = 2 was dropped with no greeting; the idle session got error: E_IDLE closing idle session; the busy one stayed open.
  2. The numbers are product decisions (a handful of sessions, minutes of idle time); what matters is that every limit exists and produces a clear message.
  3. socket.write never refuses data: it buffers and returns false past the high-water mark. For state notifications keep only the latest value per type; for events, cap writableLength and disconnect with a reason.
  4. Before a reboot, write a final line such as info: rebooting and end every socket, so scripts see a clean close and reconnect with back-off.
  5. Bound commands per second per session too: a control system stuck in a loop shouldn't starve the call.

Learn more: Building a line-based command server in Node and Testing and securing command interfaces.

How would you design the command syntax for a device's Telnet or SSH API?
Show answer

Predictable for machines first: its main reader is a regular expression written years ago. "Noun then action, volume get, volume set 42, all defined in one table that drives parsing, validation, usage text and help. Every response is one line: success echoes the resulting state in the same form as the get, and every other line starts with a tag, error:, info: or notification:, so a client classifies any line with one prefix check. Errors carry a stable code plus a human message, and only the code is a contract. Arguments are typed and range-checked, one quoting rule works in both directions so replies re-parse with the same tokenizer, and new integrators get a JSON-lines mode. Then I treat the published grammar like a public API: additive changes only."

What’s happening
  1. Poly's public guides show a similar shape: volume <get|up|down|set {0..50}>, and feedback that echoes the setting (sleeptime 30 answers sleeptime 30).
  2. Verbs case-insensitive, values not: VOLUME GET works, the room name keeps its case.
  3. A small stable code set: E_SYNTAX, E_UNKNOWN, E_USAGE, E_RANGE, E_STATE, E_FORBIDDEN, E_BUSY. It's the same split as type versus detail in problem details.
  4. No prompts, banners, pagers or aligned columns on the API port: a prompt with no newline never completes a line, and a pager waits forever.
  5. Deprecate with a replacement and keep the old command working; Poly's guide marks the sleep form of listen deprecated and recommends sleep register.

Learn more: Designing a command grammar.

volume set 042 replies volume set 042 ok, and volume set 70 on a 0–50 scale replies volume 70. What's wrong with both?
Show answer

They echo the request instead of the resulting state. "A script comparing the reply with volume get sees two different strings for the same state, and if the device clamped 70 to 50, the reply lies about what the room is set to. The reply should be the canonical state after the change, formatted exactly as get would print it: volume 42, or an error such as error: E_RANGE level must be 0..50, got 70. Then a script can compare replies directly, and the reply is the truth, not an acknowledgement."

What’s happening
  1. "Echo the state, not the request" means the reply is computed after the change, from the device's own value.
  2. Clamping silently is itself a contract decision: either reject out-of-range values (the usual choice for a published API) or document clamping and report the clamped value.
  3. The same tokenizer should parse replies: name "Board Room \"East\"" round-trips to the stored value.
  4. A rejected command must leave state unchanged; the testing chapter's transcript test caught a device that stored 70 before validating it.
  5. Never change the format of an existing reply in an update: scripts parse it.

Learn more: Designing a command grammar.

How do you mix asynchronous notifications with request and response on one Telnet or SSH session?
Show answer

Every unsolicited line identifies itself, replies come back in command order, and the client demultiplexes. "Notification lines start with a prefix such as notification:; every other line resolves the oldest pending command, first in, first out. If commands can complete out of order, a slow diagnostic next to a quick volume get, I add client-chosen tags, #7 diag run answered by #7 …. Subscriptions are per session, idempotent to re-send, and removed when the session closes. A client that assumes 'the next line is my answer' will one day take a notification as its answer and be off by one for the rest of the session."

What’s happening
  1. In the chapter's model the naive client asked volume get and received the connecting notification, then connected for mute get; the demux client got volume 30 and mute off and handled both notifications.
  2. Poly's public integrator guide recommends registrations over polling, with notify callstatus answered notify callstatus success, and says registrations are written to flash, retained across restarts, and specific to the port they were made on.
  3. Document whether an event caused by a command comes before or after its reply; either is fine, varying isn't.
  4. Each closed session must remove its listener, or a device running for months leaks a function per dead socket.
  5. Slow readers: latest value wins for state notifications, a capped buffer and a clean disconnect for events.

Learn more: Asynchronous notifications on a command channel.

How do you parse a notification line whose SIP address itself contains colons?

The device sends this line on a subscribed session:

Notification line
notification:callstatus:outgoing:1:sip:bob@example.com:connecting
Show answer

Not by splitting on colons: the SIP address contains colons, so the fields shift. "Parse from both ends: the fixed fields from the left (notification, callstatus, the direction, the call id), the fixed field from the right (the state), and treat everything in between as the variable field, the address. Values with colons are normal here: SIP URIs, IPv6 addresses, far-site names. If you're designing the format rather than consuming it, quote or escape variable fields, put variable-length fields last, or offer a JSON mode for new integrations."

What’s happening
  1. split(":") gives sip and bob@example.com as separate fields, so "state" becomes bob@example.com or the wrong index entirely.
  2. Left-fixed: type, event, direction, id; right-fixed: state; middle: address.
  3. Real Poly notification examples in the public guide are colon-separated in this style, carrying the call id, far site name and address, state and speed, which is why consumers need this care.
  4. A parser should be tested against real samples, including addresses with colons and names with spaces.
  5. New formats avoid the problem with one JSON object per line.

Learn more: Asynchronous notifications on a command channel.

Why SSH instead of Telnet for a device's command API, and what changes for the device?
Show answer

SSH encrypts the session, authenticates the device by its host key, and supports public-key login so no password crosses the network. "A machine in the middle can't impersonate a device whose host key the client has pinned, and if only public keys are accepted, password guessing disappears from the attack surface. SSH also gives channels: exec runs one command with clean stdout, stderr and an exit status, which is what automation wants, and shell is the interactive session, which can carry exactly the same line protocol as the Telnet port, so moving off Telnet is a transport change, not a rewrite. For the device it means a unique host key generated on first boot with its fingerprint visible, authorised public keys per role, algorithms current clients support, and a plan for key changes after a factory reset."

What’s happening
  1. Three layers (RFCs 4251–4254, 2006): transport with the host key, user authentication, and the connection protocol multiplexing channels.
  2. Never bake the host key into the firmware image: every device would share one identity and one leaked image impersonates the fleet.
  3. After a factory reset OpenSSH shows "REMOTE HOST IDENTIFICATION HAS CHANGED!": from the client's side a new key and an attacker look the same. Fleets record fingerprints at provisioning and distribute known_hosts, or use SSH certificates signed by a CA.
  4. A client that accepts any host key (StrictHostKeyChecking=no, hostVerifier: () => true) encrypts but doesn't authenticate the device: Telnet with extra steps against a man in the middle.
  5. In Node, ssh2 implements both server and client, and the same command layer sits behind it as behind REST and Telnet.

Learn more: SSH as the secure alternative.

A monitoring system runs callinfo all on every device once a minute. Should it open a shell and type the command, or use exec?
Show answer

exec. "The device runs exactly one command and closes the channel, so the script reads stdout to the end with no prompt detection and no 'wait until the reply looks complete' timeouts; errors go to stderr with a non-zero exit status, so 'failed' is exit != 0, not a regular expression over text the next firmware might reword. With a shell the script has to wait for the prompt, send the line, find where the reply ends, cope with banners and echo, and send exit, and every one of those is a place for an update to break it."

What’s happening
  1. In the chapter's model, exec "volume get" returned stdout volume 30 with exit 0; exec "volume set 70" returned the error on stderr with exit 2. The real OpenSSH client showed the same.
  2. Real tools (ssh host 'cmd', Ansible, CI scripts) rely on that exit status.
  3. shell is right for long-lived sessions that want notifications, like a control panel.
  4. subsystem is the third channel type, for named protocols such as SFTP for log and config transfer.
  5. Prompts are for humans; a script shouldn't have to strip them.

Learn more: SSH as the secure alternative.

How would you support REST, Telnet and SSH without three implementations of every command?
Show answer

One command layer under all of them, with thin adapters per transport that only translate syntax. "A registry of operations, each with a required role, an input schema and a handler, and one invoke function that checks existence, authorisation and validation, runs the handler and writes the audit log, always in that order. The REST adapter maps resources and JSON to commands and renders errors as problem details; the Telnet and SSH adapters frame lines, tokenize them, convert text to typed values and render codes as lines or exit statuses. Error codes are shared, so E_INVALID is a 422 on REST and error: E_INVALID … on the CLI. That's parity by construction: one range check, one role check, one audit trail, one place to test."

What’s happening
  1. Three implementations drift: the REST API checks the range and the Telnet command doesn't, SSH skips the role check, only web changes reach the audit log.
  2. In the chapter's model the same out-of-range volume produced the same zod message on all three doors, and a viewer's write was 403 on REST and E_FORBIDDEN over SSH, all in one audit format.
  3. Convert text in the line adapter, not in the shared schema: zod 4.6.7's z.coerce.number() accepts "" and null as 0, true as 1 and "0x10" as 16, so a REST {"level": null} would mute the room.
  4. The registry can generate CLI help, a capabilities command and OpenAPI fragments.
  5. Long-running commands differ only in the adapter: 202 plus an operation on REST, an acknowledgement plus notifications on the CLI.

Learn more: One command layer behind REST, Telnet and SSH.

How would you test a device's Telnet and SSH command API?
Show answer

At three levels, plus fuzzing with invariants. "The command layer gets plain unit tests: every command, every argument boundary, every role, and the most valuable assertion in a device suite, 'a rejected command doesn't change state'. The adapters get translation tests: a line reaches the right command with the right typed arguments, and codes render correctly. Then scripted session tests: readable transcripts of sent and expected lines, wildcards for messages whose wording may change, a timeout on every wait, run against a simulator in CI and real hardware in the lab, with fragmented input, oversized lines, concurrent sessions, interleaved notifications and reconnects after reboot. On top, a seeded grammar-aware fuzzer checking properties like 'only clean error codes' and 'replies re-parse to the same tokens'."

What’s happening
  1. The transcript runner in the chapter caught two real bugs: volume get reporting 70 after volume set 70 was rejected, and diag run never answering (reported as nothing within 200 ms rather than a hang).
  2. After a timeout, reconnect rather than carry on: a late reply would otherwise shift every later check.
  3. The fuzzer crashed the naive dispatcher with volume __proto__ (a prototype lookup treated as a command); Object.hasOwn makes it a clean E_USAGE. Seeded, so the failure reproduces, and shrunk to its smallest input.
  4. Five thousand clean inputs is evidence, not proof; keep the fuzzer in CI with new seeds and every failure as a regression case.
  5. Make "no unexpected port answers on a production build" a release test, so a debug shell can't ship.

Learn more: Testing and securing command interfaces.

A device drops a Telnet session after three failed passwords, and a new session gets three more. Connecting and failing three times takes about a second. How many guesses a day is that, and what would you change?
Show answer

About 259,000 a day from one loop (3 × 86,400), more with parallel sessions. "A per-session limit only costs the attacker a reconnect. What actually limits guessing is state that survives the session: failures counted per source address and per account, with growing delays or a temporary lock, and an alert. The better answers remove the problem: Telnet off by default or bound to a management network, and SSH with public keys, where there's no password to guess."

What’s happening
  1. Each cycle is connect, three wrong passwords, disconnect: three guesses a second, 3 × 86,400 = 259,200 a day.
  2. Poly's public guide documents this pattern for its Telnet port: three failures disconnect the session, and Telnet can't be locked for failed logins the way web and SSH access can. Recognise the pattern; don't copy it.
  3. Telnet also sends the password in clear: a TCP tap captured Password: Rm7-north-desk in the chapter's model, while the same login over SSH showed only the version strings and algorithm lists.
  4. Bind Telnet to the management VLAN when it must be on, and say in the UI that it's unencrypted.
  5. Clearing "Telnet Authentication" or enabling Telnet at all should be an audited event with a warning.

Learn more: Testing and securing command interfaces and Hardening.

Polling, long polling, SSE or WebSockets: which would you use for device status, and for whom?
Show answer

Different clients, different transports, all fed by one event hub inside the device. "For the admin web UI, Server-Sent Events for status and REST for commands: status flows one way, commands already have validation, ETags and error bodies, and EventSource reconnects by itself and sends Last-Event-ID. A WebSocket only where the client sends a lot too: a touch controller, a camera joystick, live meters. Long polling with a since cursor for integrations that only speak plain HTTP, the Telnet line protocol with notifications for room control systems, short polling at minutes for monitoring, and a connection the device opens outwards for the cloud, which can't connect in through the customer's firewall."

What’s happening
  1. On a device the server is a small box whose job is the call, and its listeners are a handful of very different clients, so one transport rarely fits all.
  2. Long polling needs its cursor: changes between one response and the next request aren't lost because the next request says since=N. Hold for about 25 s, below proxies' idle timeouts, then 204.
  3. Building a separate notification feature per interface means the web UI says "muted" while the control system never heard; one hub, many adapters.
  4. Poly's public REST guide (VideoOS 4.4.0, August 2025) documents status as plain request/response endpoints and no push channel; whether PolyOS devices offer one, the public documents don't say.
  5. A firmware-update progress bar is best read from the operation resource (polled every couple of seconds), because the stream dies with the reboot anyway.

Learn more: Polling, long polling, SSE and WebSockets for device status.

What bites you when you put SSE and WebSockets on a device's own web server?
Show answer

Connection limits, idle proxies, authentication, and self-signed certificates. "Over HTTP/1.1, browsers allow six connections per domain and each open EventSource holds one, so the seventh tab on the same device hangs: share one stream between tabs or serve HTTP/2. Idle connections get dropped, so send a comment heartbeat about every 15 seconds. Neither EventSource nor the browser WebSocket constructor can set an Authorization header, so streams ride on the session cookie, which makes checking Origin on the WebSocket handshake mandatory, or any page the admin visits can open a socket with their cookie. And a browser shows no warning page for a background stream to an untrusted certificate; it just fails, so keep streams on the admin page's origin."

What’s happening
  1. MDN's SSE guide (checked October 2026) says Chrome and Firefox marked the six-connection limit "Won't fix"; many embedded servers speak only HTTP/1.1.
  2. The heartbeat interval comes from the WHATWG HTML standard's own authoring note, "every 15 seconds or so".
  3. EventSource doesn't reconnect after a non-200 status or a wrong content type; a 204 is the documented way to stop it, so refusing a stream with 503 also stops the browser's retries.
  4. Cross-site WebSocket hijacking is CSRF for sockets: the browser attaches the cookie to the handshake from any origin.
  5. Commands still go to REST; SSE is one-way by design.

Learn more: Polling, long polling, SSE and WebSockets for device status and Securing the on-device web UI.

How do you design the events a device publishes?
Show answer

Separate state events from occurrences, and put every event in one envelope with a boot id and a sequence number. "A state event says 'the value is now X', audio.muted is true: only the latest matters, and a snapshot repairs a missed one. An occurrence says 'X happened', call.ended with a reason: each one matters, and a snapshot can't bring it back, so anything important is also kept in a queryable log like call history. The envelope has boot, a seq that increases by one per event within that boot, a stable namespaced type, wall-clock time for display, monotonic uptimeMs for durations, and data. Consumers order by seq, never by timestamp, and read tolerantly: skip unknown types, ignore unknown fields."

What’s happening
  1. Events are an API for years: once a partner's control system reacts to call.state becoming connected, that name, its fields and its meaning can't change.
  2. Fat events for small state (the new value), thin "changed, here's the version" events for big resources, so eight clients don't each GET after every change.
  3. Don't stream measurements like audio levels in the same sequence as call.ended: sample them on a separate, droppable channel.
  4. With one global sequence and filtered subscriptions, a gap stops meaning "I missed something": send everyone everything, or give each subscription its own sequence.
  5. Leaving the room: map onto CloudEvents (id, source, specversion, type); its optional sequence extension compares lexicographically, so zero-pad it.

Learn more: Designing device events.

Why do device events carry a sequence number if they already have a timestamp?
Show answer

Because the device's clock is wrong before NTP and jumps when it syncs, so timestamps can run backwards; sequence numbers never lie. "In the chapter's model the device booted with its clock two minutes fast; after NTP corrected it, a mute that happened later was stamped earlier than the call events before it, and sorting by time ended with call.state = connected for a call that had ended. So the rule is: sequence numbers for order, the monotonic clock for durations, wall-clock time for display. A call duration from uptimeMs was 61 seconds; from the wall clock it came out at about minus 59."

What’s happening
  1. seq increases by one per event within a boot; (boot, seq) identifies an event uniquely, which also makes duplicates harmless.
  2. uptimeMs comes from a monotonic clock, right for durations even when the wall clock jumps.
  3. time is for humans and logs, never for ordering.
  4. A change to the same value isn't an event: in the model the second setState("audio.muted", true) published nothing, so sequence numbers count real changes only.
  5. Persisting seq across reboots costs a flash write per event or batch; a boot id is the cheaper fix for restarts.

Learn more: Designing device events.

A client has a snapshot at sequence 41 and then receives events 42, 42 (a duplicate after a proxy retry), 44 and 45. What does it do with each?
Show answer

Apply 42, skip the duplicate, stop at 44 because 43 is missing, resync, then handle 45 against the new snapshot. "The rule is three comparisons: at or below my sequence, skip; exactly one more, apply; anything bigger is a gap, so stop applying and ask the device for the events since my sequence, or a fresh snapshot. If the device answers with a snapshot already at 45, the waiting 45 is skipped and the client is correct."

rq-seq.mjs
// Snapshot plus events: what a client does with each event after a snapshot at seq 41 (same boot).
let mine = 41;
const arriving = [42, 42, 44, 45];
for (const seq of arriving) {
  if (seq <= mine) console.log(`#${seq}: skip (already have up to ${mine})`);
  else if (seq === mine + 1) { mine = seq; console.log(`#${seq}: apply, now at ${mine}`); }
  else {
    console.log(`#${seq}: gap (expected #${mine + 1}): stop applying, resync since ${mine}`);
    mine = 45; // say the device answers with a fresh snapshot taken at seq 45
    console.log(`     resync: snapshot at seq ${mine}`);
  }
}
Result
#42: apply, now at 42
#42: skip (already have up to 42)
#44: gap (expected #43): stop applying, resync since 42
     resync: snapshot at seq 45
#45: skip (already have up to 45)
What’s happening
  1. #42 is 41 + 1: apply, and the client is at 42.
  2. The duplicate #42 is ≤ 42: skip. Sequence numbers make delivery idempotent.
  3. #44 is more than 42 + 1: event 43 is missing, and 44 might depend on it, so the client must not apply it. It asks for events since 42.
  4. The model assumes the device answers with a snapshot at 45 (it had moved on by then), so the waiting #45 is ≤ 45 and skipped. Had it replayed 43 and 44 from its ring instead, the client would apply them and then 45 normally.
  5. The other half of the rule: all of this is within one boot. A different boot id always means a fresh snapshot, whatever number you remember.

Learn more: Snapshot plus events.

After a device reboot, an admin's dashboard freezes: no errors, no updates. The device keeps its event sequence in memory and sends no boot id. What happened?
Show answer

The sequence restarted at 1, so the client treats every new event as "already seen" and skips them all. "The client last saw 57; the rebooted device sends 1, 2, 3, which are all at or below 57, so the skip rule throws every one away and the UI shows stale state with no error. The fix is a boot id in every event and in the hello: a different boot means a different sequence, so take a fresh snapshot. The alternative, persisting the sequence so it never goes backwards, costs flash writes, which matters on devices with limited write endurance."

What’s happening
  1. The bug is silent by construction: rule 3 of snapshot plus events (skip what you have) is doing exactly what it should, on the wrong assumption.
  2. The reverse bug exists too: a client from a long-lived old boot asks since=900 from a device that rebooted and is at 12; a since <= seq check plus the boot check catches both.
  3. Worse than freezing: an old since=8 can be a valid number for the new boot, and replaying from it would be silently wrong.
  4. With SSE, put both halves in the event id (id: b-91c2:57), so Last-Event-ID carries them on reconnect; an empty id: line clears it and forces a snapshot.
  5. Best of all, send the snapshot as the first message on the stream and subscribe in the same synchronous step, so there's no race to get wrong.

Learn more: Snapshot plus events.

How should a device's web UI behave when the device reboots or drops off the network?
Show answer

Keep showing the last known state, marked stale; tell a planned restart from a lost connection; retry at a sensible pace; and when it's back, ask what changed. "A close with 1012 'Service Restart' or 1001, or a reboot the UI started, means planned: probe at a steady pace and honour 503 with Retry-After while services start. A 1006, or the heartbeat watchdog firing, means lost: retry with exponential back-off and full jitter, switch to 'Device unreachable' after about a minute, but never stop. On reconnect, the hello decides: the same boot id, resume from my last sequence; a new boot id, take a snapshot; new firmware, reload the page, because the JavaScript in the tab was served by the old firmware."

What’s happening
  1. Close codes are from IANA's WebSocket registry: 1012 Service Restart, 1006 Abnormal Closure (seen by page code for every connection that died without a close handshake), 1013 Try Again Later.
  2. Browser code can't tell why a WebSocket failed (refused, DNS, TLS all look like 1006), so keep a plain GET /health the page can fetch to see a 503 and its Retry-After.
  3. In the chapter's simulation, the planned reboot honoured Retry-After: 8 and then reloaded for new firmware; the network drop backed off with jitter, showed "unreachable" at 72 s, and resumed from sequence 57 on the same boot.
  4. Also expect a lost session (401 after reboot: log in without losing the form), a new DHCP address, and changed configuration (an update rolled back).
  5. Don't reload the page on every reconnect: it throws away typing and loops when the device flaps.

Learn more: Reconnection and back-off.

Why does a device stream need its own heartbeat if TCP already has keepalive?
Show answer

Because a client that's only receiving never notices a silently dead connection, and TCP keepalive is far too slow by default. "When Wi-Fi vanishes, no close frame arrives and the socket looks open; a receiver has nothing to retransmit, so TCP never notices, and Linux's default keepalive waits two hours before its first probe. So the device sends an application heartbeat, say every 10 seconds, and the UI's watchdog resets on every message and closes the socket itself after about 25 seconds of silence: two and a half intervals, so one late heartbeat doesn't trigger a reconnect but two missed ones do. Browser code needs application-level heartbeats because WebSocket ping and pong are handled by the browser and invisible to the page."

What’s happening
  1. In the chapter's model the network died silently at 33 s and the watchdog fired at 55 s: detection in 22 s instead of never.
  2. Heartbeats find the failure; snapshot plus events repairs the state: the status change lost at 41 s is replayed on resume.
  3. Shorter intervals detect faster but cost a message per client per interval on a constrained device.
  4. Server-side code can use ws's ping() and pong to reap dead clients and free their slots.
  5. For SSE, the heartbeat is a comment line (: hb).

Learn more: Reconnection and back-off.

How do you serve real-time state to ten clients from a box whose real job is the call?
Show answer

Explicit limits, with a defined behaviour at each one. "A fixed number of stream slots, with refusals each client can read: 503 with Retry-After for API clients, and accept-then-close with 1013 for the browser UI, which can't see handshake statuses. A bounded queue per subscriber, checked through bufferedAmount or writableLength; when a slow client overflows it, I drop its backlog and tell it to resync from a snapshot, so memory is capped and the client stays correct. High-rate values are throttled at the source with a trailing edge, so the last value always goes out. And a small maxPayload, compression off, limits per identity, heartbeats to reap dead connections, and each event serialised once for everyone."

What’s happening
  1. The chapter's fan-out model: the raw slider produced 11 events and the slow control system needed a resync; throttled to one volume event per 250 ms, 5 events reached everyone in order.
  2. The danger is memory, not bandwidth: with the chapter's illustrative assumptions, one stalled subscriber with an unbounded queue holds 5.9 MiB after ten minutes, while bounded 64 KiB queues cap all eight at 512 KiB.
  3. ws 8.22's default maxPayload is 100 MiB, far too much for a device; with 64 KiB, an oversized message closes with 1009 and ws never buffers it. An error listener is mandatory.
  4. ws leaves permessage-deflate off by default on the server, and its README warns about its cost; keep it off.
  5. If many people need to watch one room, let the cloud hold one connection and fan out where servers can be added.

Learn more: Constrained devices and many clients.

After a partner deploys a monitoring script, admins see "too many clients" on the device's web UI. The script has one instance and polls nothing. What's happening?
Show answer

It's leaking connections: each run opens a stream and never closes the old one, and the device counts each against its slots. "After eight runs every new client, admins included, is refused. On the device I'd reap dead connections with heartbeats, so streams whose other end is gone free their slots, and limit streams per identity: the script's API key gets, say, two, and its third is refused with 1008 Policy Violation or 429 while the admins' slots stay free. Then log the identity, address and connection count so support can show the partner exactly what their script does. The fix belongs in the script, but the device must never let one client deny service to everyone."

What’s happening
  1. A global slot count alone lets one misbehaving client take every slot.
  2. Server-side ping/pong (or an expected reply to an application heartbeat) finds sockets whose peer has gone.
  3. Per-identity limits need the stream to be authenticated, by session or API key.
  4. Close idle streams too: a stream that has received nothing it asked for in an hour is probably abandoned.
  5. The same thinking applies to sessions on the REST side: scripts that log in on every call should use tokens instead.

Learn more: Constrained devices and many clients.

Walk me through what happens when a room system places a SIP video call.
Show answer

Two conversations: signalling through the proxy, media directly between the endpoints. "The room sends an INVITE to its SIP proxy with an SDP offer: the codecs it can use and the IP address and ports where it wants to receive media. The proxy answers 100 Trying and forwards it; the callee rings, 180, and answers 200 OK with an SDP answer that picks codecs from the offer and gives its own media address. The caller sends ACK, and from then on RTP, usually SRTP-encrypted, flows over UDP between the two SDP addresses, with RTCP reports carrying loss, jitter and round-trip time. The proxy only sees a handful of messages. That split is how I debug: never rings, look at registration and routing; connects with no video, look at the media path."

What’s happening
  1. SIP is RFC 3261 (2002), text and HTTP-like; SDP is RFC 8866; offer/answer is RFC 3264 (2002), the same idea in SIP and WebRTC.
  2. The answer keeps only codecs both support, in the offer's order; with none in common, the stream is rejected with port 0 or the whole call with 488 Not Acceptable Here.
  3. ACK and BYE go straight to the callee's Contact unless a proxy added Record-Route; media never follows the route set, only the SDP.
  4. H.323 does the same job with a gatekeeper and binary H.225/H.245; Poly Video mode documents both SIP (TLS recommended) and H.323.
  5. Packets cost more than the codec rate: 64 kbit/s G.711 in 20 ms packets is about 84 kbit/s on the wire once IP, UDP, RTP and the SRTP tag are added.

Learn more: How a video call works.

A call connects, but there's no audio or video, and it drops after about 30 seconds. What's your first suspect?
Show answer

NAT breaking the media address in the SDP. "Signalling worked, so registration and routing are fine; the media path isn't. The address in the SDP is whatever the endpoint believes its address is, so a room behind NAT writes its private 10.x address, the far end sends RTP there, and nothing arrives. Firewall 'SIP ALG' features that rewrite SDP on the fly often make it worse. The fixes are a session border controller or H.460 traversal for SIP and H.323, or ICE with STUN and TURN, which WebRTC makes mandatory. I'd confirm by comparing the addresses in the SDP with the addresses packets actually arrive from."

What’s happening
  1. Two paths, two failure modes: signalling through proxies, media directly at the addresses in the SDP.
  2. The drop at about 30 seconds is often the signalling side of the same NAT problem: if the ACK can't reach the private address in the callee's Contact, the callee keeps retransmitting its 200 OK for 64×T1, 32 s with the default T1 of 500 ms, and then ends the call with BYE (RFC 3261, section 13.3.1.4).
  3. For firewall traversal, Poly's guide describes H.460.18/.19 through a device such as Poly Clariti Edge, and lists the ports a strict firewall must allow out.
  4. Look at the call statistics: zero packets received in one direction is the signature. SRTP doesn't change the diagnosis: encrypted packets that never arrive are still missing.
  5. In WebRTC the equivalent is ICE never finding a working pair, often because no TURN server is configured.

Learn more: How a video call works and WebRTC concepts.

Explain how two browsers set up a WebRTC call.
Show answer

WebRTC gives each side an RTCPeerConnection but leaves signalling to the application. "So first there's a channel between the peers, usually a WebSocket to your own server. The caller creates an SDP offer and sets it as its local description, which starts ICE candidate gathering, and sends it; the callee sets it as its remote description, creates an answer that picks codecs and directions, sets that and sends it back. Meanwhile each side trickles its candidates: host addresses, its public address as seen by a STUN server, and relay addresses on a TURN server. ICE pairs them, checks them in priority order and nominates the best pair that works, falling back to the relay. DTLS then runs over that path, checked against the certificate fingerprint in the SDP, to derive SRTP keys. For more than a few people, an SFU with simulcast rather than a mesh."

What’s happening
  1. JSEP (RFC 9429, April 2024) defines the division: the browser makes SDP, the app carries it. ICE is RFC 8445, STUN RFC 8489, TURN RFC 8656, trickle ICE RFC 8838.
  2. The offer's m= lines say port 9 and 0.0.0.0: placeholders, because candidates trickle in later. A hardware endpoint that ignores ICE sends media to 0.0.0.0:9.
  3. BUNDLE puts all media sections on one transport, so a call needs one path through the firewall, not four.
  4. In the chapter's headless Chrome 156 run, the transport reported DTLS connected and SRTP_AES128_CM_HMAC_SHA1_80: WebRTC has no unencrypted mode.
  5. An SFU forwards streams without decoding (each browser uploads once); an MCU decodes, mixes and re-encodes, costing far more server CPU but sending each endpoint one stream, which is why traditional room systems used MCUs.

Learn more: WebRTC concepts.

WebRTC calls work for everyone in the office but some users at home or in hotels get no media. What's missing?
Show answer

Almost certainly a working TURN server. "In development both peers are on friendly networks, so host or server-reflexive pairs work and nobody notices TURN isn't configured. In production some users sit behind symmetric NATs, which pick a new public port per destination so the STUN-learned address is useless, or behind firewalls that block UDP, and they need a relay. I'd deploy TURN on UDP 3478 and on TLS port 443, which gets out of networks that only allow HTTPS-like traffic, give clients short-lived credentials from the server rather than static ones in the page, and watch the share of relayed calls, which is both a reliability signal and a cost line."

What’s happening
  1. Candidate types and priorities: host beats server-reflexive beats relay, so relays are only used when nothing direct works.
  2. In the chapter's ICE model: same LAN host→host; office to home host→srflx; symmetric NAT host→relay; UDP-blocking hotel Wi-Fi only relay→srflx over TURN on TLS 443.
  3. A misconfigured relay is silent: with iceTransportPolicy: "relay" and no ICE servers, Chrome gathered no candidates and stayed in new, with no error.
  4. Every relayed call costs the TURN operator bandwidth.
  5. restartIce() re-gathers with new credentials when a network changes mid-call.

Learn more: WebRTC concepts.

The dashboard says the video stream has 1% packet loss. Is that "1% bad video"?
Show answer

No: at 1% loss about 7% of frames are damaged, about 122 a minute, because one lost packet damages a whole frame. "A 2 Mbit/s, 30 fps stream sends about 7 packets per frame, and a frame survives only if all 7 arrive, so the damaged share is 1 − (1 − p)^7. And it's worse than the count, because compressed frames refer to earlier ones, so damage spreads until a keyframe arrives. One NACK retransmission brings it down to about 1.3 damaged frames a minute, as long as the round trip fits in the receiver's buffer."

rq-video-loss.mjs
// Why "1% packet loss" is much worse than 1% bad video: a 2 Mbit/s, 30 fps stream sends about
// 7 packets per frame, and one lost packet damages the whole frame. Losses assumed independent.
const PER_FRAME = 7, FPS = 30;
const damaged = (p) => 1 - (1 - p) ** PER_FRAME;          // share of frames with at least one lost packet
for (const p of [0.001, 0.01, 0.02]) {
  const perMin = damaged(p) * FPS * 60;
  const withNack = damaged(p * p) * FPS * 60;             // lost only if the original and one resend are both lost
  console.log(`loss ${(p * 100).toFixed(1)}%: ${(damaged(p) * 100).toFixed(1)}% of frames, ${perMin.toFixed(1)} damaged/min; with one NACK resend ${withNack.toFixed(2)}/min`);
}
Result
loss 0.1%: 0.7% of frames, 12.6 damaged/min; with one NACK resend 0.01/min
loss 1.0%: 6.8% of frames, 122.3 damaged/min; with one NACK resend 1.26/min
loss 2.0%: 13.2% of frames, 237.4 damaged/min; with one NACK resend 5.03/min
What’s happening
  1. Seven packets per frame comes from the overhead arithmetic: 2 Mbit/s at 30 fps is about 8.3 KB per frame, split into packets of up to 1,200 bytes.
  2. At 1% loss, 1 − 0.99^7 ≈ 6.8% of frames lose at least one packet: 0.068 × 30 × 60 ≈ 122 a minute, the chapter's number.
  3. With one retransmission a packet is lost only if both copies are, p², so 1.26 damaged frames a minute. This assumes independent losses; bursts break the assumption, and retransmission does nothing on a 300 ms path.
  4. The receiver's nack pli (Picture Loss Indication) asks for a fresh keyframe, which is large and costs a burst of bandwidth.
  5. Audio behaves differently: Opus in-band FEC rebuilt all but 0.2% of packets at 5% random loss, but left 3.5% missing when the same 5% came in bursts. The shape of the loss matters as much as the percentage.

Learn more: Media quality and diagnostics.

A customer says video is choppy in one meeting room. How do you investigate?
Show answer

Narrow it down, then read the room's per-stream call statistics for a bad call and match the pattern to a cause. "Every call or some, every provider or one, both directions or only what they receive? Then received loss and jitter, the bitrate actually used against what was negotiated, frame rate and resolution, and round-trip time. High loss or jitter with normal bitrate points at the network, usually the room's own link or Wi-Fi; low bitrate with no loss suggests congestion control backing off or a call-speed cap; loss in one direction points at that path; a low frame rate with a clean network suggests the far end or a CPU or camera limit. I'd compare against targets like Microsoft's for Teams: under 1% loss and 30 ms jitter from client to their edge."

What’s happening
  1. On Poly systems the public docs give three places to read statistics: the web UI's Diagnostics > Call Statistics, the REST GET /rest/conferences/<confID>/mediastats, and advnetstats on the command line.
  2. Microsoft's Teams targets (Microsoft Learn, updated September 2026): one-way latency under 50 ms, round trip under 100 ms, loss under 1% in any 15 s, burst loss under 10% in any 200 ms, jitter under 30 ms.
  3. Check which number you're reading: in the REST guide's sample, received video lost 19 of 1,249 packets (1.52%) while the device's own percentPacketLoss said 0, and the guide doesn't define either field's window.
  4. Averages hide spikes people hear: show recent intervals, keep maxima (maxJitter), look at burst loss.
  5. If the room runs Teams, the Teams admin center and Call Quality Dashboard give the provider's view of the same calls.

Learn more: Media quality and diagnostics.

A room shows 1 ms jitter and no loss, but people talk over each other. Round-trip time is 400 ms. What's going on?
Show answer

A latency problem, not a jitter problem: the path is smooth but long or heavily buffered. "Jitter measures the variation in transit time, RFC 3550's smoothed difference between arrival gaps and send gaps, so a constant delay of any size gives jitter near zero. 400 ms round trip is about 200 ms each way before encoding, buffering and decoding, four times Microsoft's 100 ms target. Usual causes are media hairpinning through a far-away VPN concentrator or relay, a VPN that isn't split-tunnelled for media, or a queue that's always full. I'd check which path the media takes and whether delay grows under load; the fix is routing, not a bigger jitter buffer, which only adds delay."

What’s happening
  1. RFC 3550's estimator: for each packet, D = arrival gap minus send gap, and jitter += (|D| − jitter) / 16.
  2. In the chapter's model a constant 120 ms delay gave 0.0 ms jitter; Wi-Fi with occasional 30–60 ms spikes gave about 12.6 ms.
  3. A delay ramping from 40 to 140 ms as a queue fills showed only 0.4 ms jitter: bufferbloat shows up as rising RTT, which is why statistics pages need both.
  4. Jitter buffers trade delay for completeness: a fixed 60 ms buffer played every packet on that Wi-Fi but added 60 ms to every word.
  5. The numbers that tell you whether audio broke up are the receiver's late or concealed packets and its buffer delay, not jitter alone.

Learn more: Media quality and diagnostics.

#Security and versioning questions

Added

Same deck, two subjects that interviewers for device platforms treat as one: what you may change in an API that years of firmware and integrations depend on, and who may call it and how they prove it. For each card, check that your answer named the rule, the mechanism that enforces it (a header, a pin, a table, a signature) and what happens to the client that doesn't follow it.

The mental model: a fleet is a museum of your releases, and every exhibit has a door. You can add rooms, but you can't repaint an exhibit someone relies on, and every door needs a lock that still works when the visitor is a ten-year-old script.

This group covers why devices make versioning hard, where the version goes, compatible change, capability discovery, deprecation and sunset, testing across firmware, then authentication, TLS without a public name, the on-device web UI, hardening, least privilege and audit logs, and signed updates.

Why is versioning harder for a device API than for a web API?
Show answer

Because the server is the device, and a fleet runs many versions of it at once, for years. "A web service deploys once and every client sees the new version; a fleet has hardware that can't take the new release, customers who pin versions for change control or partner certification, devices that are rarely online, staged rollouts and one-way upgrades. So compatibility has to work in two directions: old clients against new firmware, which the firmware owes them, and new clients against old firmware, which the client handles by discovering what each device supports. Poly's public 5.0 notes are a live example: 5.0.0 is one-way, only the second-generation models can run it, and 4.x releases continue alongside it."

What’s happening
  1. HP's official release notes (read 27 October 2026): only the Studio G62, X32, X52 and X72 support 5.0.0; "once you update your system to Poly VideoOS 5.0.0, you cannot downgrade"; systems must be on 4.6.0 or later first (older ones install 4.6.0 automatically on the way). HP's community post for VideoOS 4.7.0 (18 June 2026) says that release shipped for all providers, on both generations.
  2. The chapter's seeded fleet model: after a year 47% of devices were still on 7.x, and the oldest firmware a client must support to reach 99% of devices was still the oldest one. The numbers are invented; the long-tail shape is the point.
  3. Whoever ships later carries the burden: new firmware carries old clients, new clients carry old firmware.
  4. The on-device web UI looks like the easy case, but an admin's tab loaded before an update is an old client talking to new firmware after the reboot, so the UI should notice the version change and reload.
  5. Below the support window, fail clearly: "this device runs 6.4; update to 7.0 or later to manage it here", not a half-working page.

Learn more: Why devices make versioning hard.

A 2021 room-control integration recalls camera presets; a new cloud dashboard shows a microphone-zones panel only firmware 8.0 supports. Both run against 7.0, 7.3 and 8.0 devices. Who has to do the work?
Show answer

The firmware owes the old integration; the new dashboard owes the old firmware. "The 2021 integration can't change, nobody will update it, so every newer firmware must keep its requests working and its response fields present with the same meaning: that's backward compatibility, and it's the firmware team's job. The dashboard will meet 7.0 and 7.3 devices that have no microphone zones and never will, so it must find out per device whether zones exist and hide or explain the panel: that's capability discovery, the client's job. Each obligation lands on the only side that can still change."

What’s happening
  1. Old client meets new firmware: the device must keep old behaviour.
  2. New client meets old firmware: the client must detect and degrade, because that firmware is already shipped.
  3. The firmware team can't help the dashboard on old devices, and the dashboard team can't fix someone else's 2021 code.
  4. A test matrix with a row for the old integration is the only place its behaviour is tested at all, because its owner isn't going to.
  5. The general rule in one line: whoever ships later carries the burden.

Learn more: Why devices make versioning hard and Capability discovery.

Where would you put the version in a device API?
Show answer

A major version in the URL, kept for years, with only additive changes inside it. "/api/v1 is visible in logs and docs, caches correctly with no extra headers, works from a browser link, an <img> or any control system's HTTP client, and is trivial to route on a small on-device server. There are no minor versions on the wire: clients learn about new features from a capabilities endpoint. Each firmware advertises the majors it serves, a client picks the highest both sides know, and requests that name no version get the oldest behaviour, because those come from the oldest clients. A header or vendor media type only when one resource needs a new representation, and then always with Vary."

What’s happening
  1. Defaulting to the newest version would break every client written before versions existed on the day v2 ships.
  2. Unsupported versions fail with the status that fits where they were asked: 404 for /api/v3/…, 406 for an unknown media type, 400 for a bad or conflicting header, each listing the supported versions.
  3. Removing v1 from firmware is a much bigger decision than adding v3: every v1-only client fails on every device that installs that release.
  4. Command interfaces need versions too: a greeting line, a version or capabilities command, or format json, with the oldest behaviour as the default.
  5. Poly's public REST guide documents paths under /rest/ with no version segment, versioned by software release (the guide describes VideoOS 4.4.0); how Poly handles breaking changes isn't in the public docs, so say that, then give the pattern.

Learn more: Versioning strategies: URL, header and media type.

A header-versioned API returns v1 or v2 from the same URL, with max-age=5 and no Vary. An old status page in one tab sometimes shows "volume: NaN". Why?
Show answer

The browser's cache keys the response on the URL alone, so for up to five seconds whichever version was fetched first is served to both pages. "The new admin UI asks with Api-Version: 2 and the cache stores the v2 body, where volume is an object; the old page asks the same URL without the header within five seconds and gets the cached v2 body, and Math.round of an object is NaN. It only happens when the requests land in that order inside the cache lifetime, which is why it can't be reproduced with curl. The fix is Vary: Api-Version on every response from that URL, or moving the version into the URL, which puts it in the cache key."

What’s happening
  1. Without Vary, the cache key is method plus URL; the request header that chose the version isn't part of it.
  2. Vary: Accept, Api-Version adds those headers' values to the key, so v1 and v2 are stored separately.
  3. Any cache in the path can do this: a corporate proxy, the browser's HTTP cache, a service worker in the admin UI.
  4. A second header-versioning trap: plain links (log downloads, camera snapshots in <img>) can't set headers, so they always get the default, oldest version.
  5. URL versioning avoids the whole class of bug.

Learn more: Versioning strategies: URL, header and media type.

What rules keep a device API compatible within a major version?
Show answer

The provider only adds; the reader only reads what it needs; enums are open; types, units, meanings and defaults never change. "New endpoints, new optional request fields and new response fields are fine. Nothing is removed, renamed or given a new meaning, and status and error codes stay the same. Clients are told from day one to ignore unknown fields and to show unknown enum values rather than crash, Martin Fowler's tolerant reader. On writes I'm strict: old firmware that receives a setting it doesn't know rejects the whole write with a list of unknown fields, so an admin never sees 'saved' for something that wasn't. Renames are additive, with both names filled, and removal waits for a new major. Security fixes can justify a break, but then secure by default with an explicit, logged compatibility switch."

What’s happening
  1. Compatibility is about what clients observe: names, types, units, meanings, possible values, status codes, defaults, even timing.
  2. In the chapter's model a strict client failed on firmware 7.3's new framingZone field and on 8.0's new "group" value; the tolerant one passed and showed "Other (group)". Both rightly failed when a bad build turned presets from a number into an array.
  3. A new enum value in a fixed <select> breaks quietly: Chrome leaves value as "" and FormData omits the field. Build options from what the device reports and always render the current value.
  4. Changing a default is breaking: a factory-reset device that starts auto-answering calls is a change clients see.
  5. "Additive" that still breaks: adding pagination to a list that returned everything, or returning null where a field was always present.

Learn more: Rules for compatible change.

Planned for firmware 8.1: (a) GET /status omits sip when SIP isn't configured instead of returning "sip": null; (b) volume's maximum rises from 100 to 120; (c) autoAnswer defaults to true after a factory reset. Safe, risky or breaking?
Show answer

(a) breaking, (b) safe for writers but risky for readers, (c) breaking. "Absent and null are different shapes: a client that checks status.sip === null now sees undefined and takes the wrong branch or throws, so keep returning null. Old writers only ever send 0 to 100, still valid, but old readers may now see 110 and draw a volume bar past its end or reject it, so report the range in the capabilities document. And a default is behaviour clients observe: rooms reset to factory settings would start answering calls by themselves, which belongs in a new major version or behind an explicit setting."

What’s happening
  1. Ask what the strictest reasonable client would do with each change, not what the diff says.
  2. (a) looks like tidying up and is a shape change.
  3. (b) is the direction rule: widening a request range is safe for writers; readers were told values couldn't exceed 100.
  4. (c) is also privacy-relevant, which makes it a change you announce loudly even in a major release.
  5. A new response field, by contrast, is the textbook compatible change for tolerant readers.

Learn more: Rules for compatible change.

A new cloud manager pushes { "volume": 40, "noiseBlock": true } to a device on firmware 7.2, which has never heard of noiseBlock. What should the device do?
Show answer

Reject the whole write with a list of unknown settings, unless the client asked for lenient handling. "The worst answer is the common default: silently ignore the unknown field and return 200, so the admin sees 'Saved' for a setting that was never applied. Applying volume but not noiseBlock would leave the device half-configured while the request 'failed', so the write is all or nothing: 422 with unknownSettings, because the JSON is fine but its content isn't acceptable to this firmware. If the client sent Prefer: handling=lenient (RFC 7240), it has said it can cope with partial application, so the device applies what it knows, answers 200 with Preference-Applied: handling=lenient and lists what it ignored, and the manager can say 'applied, except noise block: not supported on 7.2'."

What’s happening
  1. RFC 7240 (June 2014) defines handling=strict and handling=lenient as preferences the server applies at its discretion.
  2. Leniency covers unknown fields only: an out-of-range value for a known setting still fails in any mode.
  3. Strict by default is a choice for configuration writes, because a silent partial success is worse than an error.
  4. Better still, the manager discovers capabilities first and doesn't send what a device can't take.
  5. The same rule applies to configuration imports: skip with a reason, never silently.

Learn more: Rules for compatible change.

Firmware 7.3 needs volume (0–10, since the first release) to become a 0–100 percentage. How do you do it?
Show answer

Add volumePercent, keep volume, and fill both from one stored value for the life of the major version. "Never redefine volume: a client reading the wrong scale doesn't fail, it sets the room to 7% instead of 70%. The device stores one value and derives both fields from it on every read, so they can't drift; an old client writing { volume: 4 } stores 40, a new one writes volumePercent directly, and a client sending both inconsistently gets a 422 naming the fields. Mark volume deprecated in the spec and remove it only in a new major, once fleet data says almost nobody uses it."

What’s happening
  1. Expand and contract on a web API ends with the contract step; on a device API it usually never comes, because shipped firmware returns the old field forever.
  2. The trap in the chapter's model: an old admin page that reads the form and saves it back unchanged sent { volume: 5 }, and the device stored 50; nobody touched the volume and it moved from 45 to 50.
  3. Fixes: ignore an old-field write whose value equals what the device would currently return, or have clients send only changed fields, which merge patch makes natural.
  4. "Save without changes changes nothing" belongs in the compatibility suite.
  5. OpenAPI 3.1 has deprecated on operations and parameters, and schemas can use JSON Schema's deprecated annotation.

Learn more: Rules for compatible change.

How does your admin UI know what a device supports, and what's wrong with if (Number(firmware) >= 7.3)?
Show answer

It asks the device, and renders from features, never from version numbers. "Two devices on the same firmware can differ by hardware model, provider mode, licence, connected peripherals and admin policy, so the UI reads a capabilities document with stable feature names, a version per feature, allowed values and limits, and a reason when something is off. Firmware from before the endpoint falls back to a frozen table keyed by version, which is safe because released firmware never changes. And the version check itself is buggy: Number("7.10") is 7.1, which is older than 7.3."

rq-versions.mjs
// Two version-comparison mistakes that show up in device clients, and the fix.
console.log(`Number(): is 7.10 newer than 7.3? ${Number("7.10") > Number("7.3")}  (Number("7.10") is ${Number("7.10")})`);
console.log(`strings : is "1.10.0" < "1.9.3"? ${"1.10.0" < "1.9.3"}  (compared character by character)`);
const cmp = (a, b) => {
  const [x, y] = [a, b].map((v) => v.split(".").map(Number));
  for (let i = 0; i < Math.max(x.length, y.length); i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
  return 0;
};
console.log(`numeric parts: 7.10 newer than 7.3? ${cmp("7.10", "7.3") > 0}; 1.10.0 newer than 1.9.3? ${cmp("1.10.0", "1.9.3") > 0}`);
Result
Number(): is 7.10 newer than 7.3? false  (Number("7.10") is 7.1)
strings : is "1.10.0" < "1.9.3"? true  (compared character by character)
numeric parts: 7.10 newer than 7.3? true; 1.10.0 newer than 1.9.3? true
What’s happening
  1. Number("7.10") parses a decimal number, so the trailing zero disappears and 7.1 sorts before 7.3.
  2. String comparison goes character by character: "1.1…" versus "1.9…" decides it at the third character, so 1.10.0 looks older than 1.9.3.
  3. Splitting on dots and comparing the parts as numbers gets both right; semver precedence adds rules for pre-releases on top.
  4. Capability discovery avoids the comparison altogether. In the chapter's model, version guessing was wrong for Gen 1 hardware on 7.3 and for a device in partner mode, where the device itself reported no dial pad.
  5. Poly's public REST guide has a small version of the idea: GET /rest/conferences/capabilities, and a per-conference capabilities object for what's possible right now.

Learn more: Capability discovery.

Your UI knows camera.tracking versions 1 and 2. A new device reports version 3 with a value presenter you've never seen. What do you do?
Show answer

Use it as version 2 and build the dropdown from the reported values, including presenter, which is only safe if feature versions are additive. "Hiding camera tracking on the newest devices because they're 'too new' would be the worst outcome a compatibility scheme could produce. If the capability document's own specification says a higher feature version does everything the lower one did, plus more, every request the UI knows for version 2 still works. An incompatible redesign gets a new feature name, such as camera.trackingZones, and the old name keeps being reported for as long as it works."

What’s happening
  1. The unfamiliar value appears in the UI with a fallback label built from the raw value, the same tolerant-reader rule as for response enums.
  2. Write the additive rule into the capability document's specification, or every client guesses, and some guess "refuse".
  3. Supported, available and allowed are three answers: hide the control, disable it, or disable it with "Disabled by your administrator". One boolean canX collapses them.
  4. Static capabilities belong in the document (cached with an ETag, refreshed on a change event); "possible right now" belongs on the call resource.
  5. For pushed events, the reverse also helps: clients declare the features they understand on subscribe, and the device downgrades or filters what they can't handle.

Learn more: Capability discovery.

How do you deprecate and remove part of a device API, and what does a Sunset date mean on a device?
Show answer

Announce in two steps with standard headers, keep behaviour identical, measure who still uses it, and remove only in a new major firmware line. "The firmware sends Deprecation (RFC 9745) on the old resource with a rel="deprecation" link to the migration guide, and Sunset (RFC 8594) with a link to the retirement policy; the resource keeps working exactly as before. On a device nobody can switch anything off on a date: firmware already shipped serves the old API for as long as it runs, so Sunset honestly means 'releases after this date may remove it', and because the headers are baked into firmware, I pick dates I can keep. Brownouts are impossible on customers' devices; instead a later release can turn the old feature off by default for new installs while keeping it on, behind a setting, for upgraded devices that use it."

rq-deprecation.mjs
// Reading Deprecation (RFC 9745: "@" + Unix seconds) and Sunset (RFC 8594: an HTTP-date) from a response.
function read(headers) {
  const deprecated = new Date(Number(headers.deprecation.slice(1)) * 1000);
  const sunset = new Date(headers.sunset);
  const ok = sunset >= deprecated; // RFC 9745 section 4: Sunset must not be earlier than Deprecation
  return `deprecated ${deprecated.toISOString().slice(0, 10)}, sunset ${sunset.toISOString().slice(0, 10)}: ${ok ? "consistent" : "CONTRADICTORY, ask the vendor; treat as possibly gone now"}`;
}
console.log("v1 audio:  ", read({ deprecation: "@1782863999", sunset: "Fri, 31 Dec 2027 23:59:59 GMT" }));
console.log("mystery:   ", read({ deprecation: "@1798761599", sunset: "Wed, 31 Dec 2025 23:59:59 GMT" }));
Result
v1 audio:   deprecated 2026-06-30, sunset 2027-12-31: consistent
mystery:    deprecated 2026-12-31, sunset 2025-12-31: CONTRADICTORY, ask the vendor; treat as possibly gone now
What’s happening
  1. The two headers use different date formats for historical reasons: RFC 9745 (March 2025) uses a Structured Field Date, @ plus Unix seconds; RFC 8594 (May 2019) an HTTP-date.
  2. @1782863999 is 30 June 2026 23:59:59 UTC, before its 31 December 2027 sunset: consistent, the chapter's example.
  3. The second pair is the chapter's quiz: deprecated at the end of 2026 but sunset a year earlier, which RFC 9745 section 4 forbids. Report it with the exact headers; meanwhile RFC 8594 says a past sunset is safest read as "now".
  4. Usage data lives on each device, so it only helps if devices report deprecated-feature use (with the client identity) to the management service; the admin UI can then say "2 integrations used the deprecated v1 audio API in the last 7 days".
  5. People need other channels too: deprecated in OpenAPI, release notes with standing Deprecated and Removed sections, SDK @deprecated tags, and direct contact with the partners in the usage list.

Learn more: Deprecation and sunset.

How do you test compatibility across firmware versions?
Show answer

A matrix: every supported client, including recorded partner integrations, against every firmware in the support window. "On pull requests it runs against simulators built from one factory, with each release's differences written down explicitly, and against recorded fixtures: responses captured from real devices on each released firmware, in every state that matters. Released firmware never changes, so those recordings stay valid forever. Nightly it runs on lab devices; release candidates get hardware-in-the-loop. Before a release I diff its API against the previous one twice, the OpenAPI specs and the recordings, because recordings catch changes nobody wrote down, like a number turning into a string. The matrix runs with fail-fast off, and a red cell blocks the release that caused it until someone decides explicitly."

What’s happening
  1. In the chapter's matrix, 3 of 18 cells failed: the 2021 partner script got 403 writing to 8.0 after a CSRF hardening, and a strict cloud agent failed on 7.2 (no v2) and on 8.0 (new enum value).
  2. The recordings diff found mics[].id turning from a number into a string, which a spec diff would miss if the spec still said integer.
  3. Simulators drift from real firmware: replay the recordings against the simulator too, and fail the build when they disagree.
  4. Keep the matrix honest without making it endless: bound it by the support window, weight by fleet share, use pairwise testing for configurations.
  5. In GitHub Actions, fail-fast: false matters: by default a failing matrix job cancels the rest, and a compatibility grid is only useful when you see all of it.

Learn more: Testing compatibility across versions.

How should a device API authenticate its callers?
Show answer

Separate people from programs. "Admins log in through the web UI and get a short session: a random id in an HttpOnly, Secure, SameSite=Strict cookie, with idle and absolute timeouts, and CSRF protection because cookies are sent automatically. Integrations get their own API tokens, minted by an admin, scoped to a job like status:read or calls:control, stored on the device only as a hash, listed in the UI and revocable one at a time. Everything runs over TLS, so Basic auth is only a compatibility option inside HTTPS. I wouldn't put JWTs on a single device: it can look up its own sessions for free and revoke them instantly; signed tokens make sense when someone else, like the fleet service, issues them."

What’s happening
  1. The front-desk model: visitors get a day badge (session), contractors a key card for specific doors (scoped token); nobody gets the master key.
  2. The device stores a slow salted hash of the password (scrypt in the chapter's model) and compares with timingSafeEqual; a token of 256 random bits can be stored as a plain SHA-256, because there's nothing to guess.
  3. Prefixed tokens (dak_…) are easy for secret scanners to find; only a session can mint tokens, and no token can be granted tokens:manage.
  4. Basic is an encoding, not encryption: the header decodes to admin:password with no key. Digest doesn't encrypt the request either; it's a legacy option, not a substitute for TLS.
  5. Poly's public REST guide shows the session pattern: POST /rest/session with user and password, a session with a sessionId and role, and 403 for a failed login. It doesn't describe scoped tokens.

Learn more: Authenticating admins and integrations.

A monitoring token scoped to status:read calls the reboot endpoint; later the admin revokes it and the script reads status again. What does the device return each time?
Show answer

First 403 Forbidden with insufficient_scope; after revocation 401 Unauthorized with invalid_token. "On the first call the token is genuine and current, so authentication succeeded and authorisation failed: 403, with WWW-Authenticate: Bearer realm="device", error="insufficient_scope", scope="system:control", the format RFC 6750 defines, so the integrator knows which scope to ask for and doesn't retry. After revocation the device can't find the token's hash, so the caller is no longer authenticated: 401 with error="invalid_token", which tells the client to get a new token rather than retry."

What’s happening
  1. 401 is "I don't know who you are"; 403 is "I know who you are and the answer is no". Mixing them up sends scripts into re-login loops.
  2. On the login endpoint, return the same error for "no such user" and "wrong password", so it doesn't confirm which accounts exist.
  3. Revocation is one delete, and the admin's session and other tokens carry on; compare a shared admin password baked into five integrations, which nobody ever rotates.
  4. Whatever a product chose (Poly's guide returns 403 for a failed POST /rest/session), partners code against it: document it and keep it stable.
  5. Tokens never go in URLs: query strings end up in proxy logs, browser history and Referer headers.

Learn more: Authenticating admins and integrations.

How do you do TLS on a device that only has an IP address?
Show answer

Encrypt from first boot with a self-signed certificate, and make clients authenticate the device by pinning or by the organisation's own CA, never by turning verification off. "Public CAs won't issue a trusted certificate for a private IP or an internal name, so the device generates its own key at first boot and serves a certificate whose subject alternative names include its addresses and local host name. Clients pin it: trust on first use with the fingerprint compared against the device's screen, or a fingerprint delivered by provisioning. Enterprises have the device create a CSR, their CA signs a certificate for its DNS name, and managed laptops trust it with no prompt. I'd pin the public key rather than the certificate so renewals don't break clients, treat a key change after factory reset as an event an admin confirms, and make sure the device has a correct clock before it validates anyone else's certificate."

What’s happening
  1. The CA/Browser Forum's Baseline Requirements haven't allowed publicly trusted certificates for reserved IPs or internal names since a phase-out ended in 2016. Poly's guide says its systems ship a self-signed certificate and describes the CSR procedure to replace it.
  2. rejectUnauthorized: false "fixes" the error and turns off server authentication: in the chapter's model the impostor on the LAN got 200 and the monitoring token. NODE_TLS_REJECT_UNAUTHORIZED=0 does it for every connection in the process.
  3. The pinned client refused the impostor during the handshake, before any HTTP was sent, so the secret never left the client.
  4. Names are checked against the SAN, not the CN: a DHCP address change fails the name check even with a perfect pin.
  5. Public certificate lifetimes are shrinking (CA/Browser Forum ballot SC-081v3: 200 days from 15 March 2026, 100 from 2027, 47 from 2029), so a device with a public name needs automated renewal.

Learn more: TLS on devices.

A client pins a device by its certificate's SHA-256 fingerprint. The certificate is renewed with the same key; later the device is factory-reset. Which events break the pin, and what should it pin instead?
Show answer

Both break a certificate pin; a public-key (SPKI) pin survives the renewal and still catches the reset. "A certificate fingerprint hashes the whole certificate, so a new serial, new dates and a new signature change it even with the same key. The SPKI hash covers only the public key, so a same-key renewal leaves it identical and the client carries on. A factory reset generates a new key, so the SPKI pin fails and the client stops and asks an admin to re-confirm, which is right: a new key is indistinguishable from an impostor until someone with another source of truth confirms it."

What’s happening
  1. In the chapter's model: after renewal, cert pin MISMATCH, key pin ok; after factory reset, both mismatch and the client stops.
  2. So design the device to keep its key across renewals; a device that makes a new key every renewal teaches admins to click "accept" without reading.
  3. In a managed fleet, the management service can publish the new key after a reset (it saw the device enrol), removing the human step.
  4. Keep the private key in hardware-backed storage such as Android Keystore or a secure element.
  5. HTTP Public Key Pinning on the open web was abandoned because sites bricked themselves; for a client pinning one device it controls, it works well.

Learn more: TLS on devices.

How do you protect a device's admin web UI from CSRF and DNS rebinding?
Show answer

Assume the attacker can't reach the device but the admin's browser can, and check three things on every state-changing request. "Where did it come from: Sec-Fetch-Site: same-origin or an Origin that's one of the device's own. Which name was used: a Host header that isn't one of the device's own names or IPs gets 421 Misdirected Request, which is what defeats DNS rebinding, because there the browser thinks the request is same-origin. And a per-session CSRF token that pages on other sites can't read. Cookies are __Host-, Secure, HttpOnly, SameSite=Strict, GET never changes state, the UI sends frame-ancestors 'none', and integrations use bearer tokens, which aren't ambient, so they skip CSRF checks."

What’s happening
  1. On an IP origin the "site" is the IP itself, so SameSite=Strict does stop pages on other devices and the internet from riding the cookie, but not XSS on the device itself.
  2. HSTS doesn't apply: RFC 6797 tells browsers to ignore it from IP-literal hosts, so redirect HTTP to HTTPS on the device or turn HTTP off.
  3. Secure cookies need HTTPS, another reason HTTPS must be on by default even with a self-signed certificate.
  4. Requiring application/json on state-changing calls means a cross-site fetch needs a CORS preflight the device never approves; never reflect Origin with Access-Control-Allow-Credentials: true.
  5. Poly's REST API added an XSRF token from VideoOS 4.6.2, per HP's community FAQ: the double-submit pattern, a readable XSRF-TOKEN cookie copied into an X-XSRF-TOKEN header.

Learn more: Securing the on-device web UI.

A room name set over the Telnet API contains <img src=x onerror=…>. Why is that the web team's problem?
Show answer

Because the web UI displays strings that arrive through channels the web team never sees, and stored XSS defeats every CSRF defence. "If the UI builds HTML by concatenation, that room name runs script in the device's origin, with the admin's session, the page's CSRF token and every API the session can call. Two layers fix it: validate on input at every interface, with the same schema for the CLI, provisioning and the web form, so a room name has a length and a character set; and escape on output in the UI, because input validation will eventually miss a channel. A strict CSP with no unsafe-inline is the third layer: even if markup gets in, inline handlers like onerror don't run."

What’s happening
  1. Room names, contact names, a caller's SIP display name, a meeting title: all reach the UI from somewhere else.
  2. React and Angular escape interpolated text; the danger moves to dangerouslySetInnerHTML, [innerHTML], bypassSecurityTrust… and HTML built for exports.
  3. In the chapter's model, escaping the five characters & < > " ' turned two created tags into one.
  4. XSS is why SameSite alone isn't enough: injected script runs same-origin and same-site.
  5. This is the security argument for one command layer behind every door.

Learn more: Securing the on-device web UI and One command layer behind REST, Telnet and SSH.

What do you check first when hardening a device?
Show answer

Credentials and exposure. "No universal default password: each unit gets a unique random secret that isn't derivable from public information like its MAC address, and it does nothing useful until an admin sets a real password, checked by length and a blocklist rather than composition rules, as NIST SP 800-63B-4 says. Login on every interface is rate-limited in a way that slows attackers without locking out the real admin. Then an inventory of every listening port and service, with what the customer hasn't asked for turned off (Telnet, SNMP v1/v2c, debug ports, adb over the network), unauthenticated responses that don't reveal versions, and TLS on with a sane minimum. I'd measure against ETSI EN 303 645 and the OWASP IoT and API Top 10, so it's a checklist, not opinion."

What’s happening
  1. The 2016 Mirai botnet spread by logging in over Telnet with a list of 62 factory credential pairs, peaking around 600,000 infections (Antonakakis et al., USENIX Security 2017).
  2. ETSI EN 303 645 (V3.1.3, 2024-09) opens with "No universal default passwords" (5.1-1: unique per device or user-defined), asks that unused interfaces be disabled (5.6-1) and that unauthenticated disclosure of security-relevant information be minimised (5.6-2).
  3. NIST SP 800-63B-4 (July 2025): at least 15 characters when the password is the only factor, a blocklist of common and context-specific values (the device's own serial and room name), and no composition rules.
  4. For reference, Poly's public 4.7.0 guide says the default admin password is the last six characters of the serial number, unique per unit, and strongly recommends setting one; depending on system and version you may be prompted to change it at first sign-in.
  5. Say less to strangers: no firmware version on the login page, app.disable("x-powered-by"), discovery that advertises a service type, not a model-and-version string.

Learn more: Hardening.

A reviewer asks you to lock the admin account for 30 minutes after five failed logins. The device has one local admin and its UI is reachable from the whole corporate network. What's the problem?
Show answer

Anyone on the network can keep the only admin locked out forever by sending five bad passwords every 30 minutes. "Lockout counts failures for the account, not the person, and the username on a device is documented. In the chapter's simulation, a 15-minute lock cut the attacker to 480 guesses a day and locked the real admin out both times. I'd propose per-source exponential back-off, which slowed one attacker to 105 guesses a day without touching the admin, plus an account-wide ceiling for many-address attacks, a recovery path the network can never lock (the local touch interface or a physical reset), alerts on throttling, and above all a strong unique password, so even 2,100 guesses a day from twenty addresses are harmless."

What’s happening
  1. NIST SP 800-63B-4 suggests growing waits instead of lockout, with a hard ceiling of at most 100 consecutive failures before the authenticator is disabled; on a one-admin device that ceiling needs a lock-proof recovery path.
  2. Per-source limits scale with the number of addresses an attacker controls: limited on a LAN, not on the internet, which is why the admin interface shouldn't face the internet (ETSI 5.6-1's first example).
  3. Return 429 with Retry-After rather than silently dropping, and log every lock as an audit event.
  4. A per-connection limit isn't a limit: the Telnet pattern of three tries per session costs an attacker one reconnect.
  5. The rate limit is the second wall; the password is the first.

Learn more: Hardening.

How would you design authorisation for a device API?
Show answer

One policy table, consulted by every interface, that maps roles and token scopes to each setting and action. "Reads are filtered per field and secrets are write-only, so nobody, not even an admin, can read a SIP or Wi-Fi password back; they can only replace it. Writes are validated against the schema: an unknown field like isAdmin is a 422, and a forbidden field rejects the whole change with a 403 that names it. Destructive functions like factory reset are admin-only and ask for the password again. In a fleet service I'd also check ownership of every id in the path, so one tenant can't touch another's devices. And hiding a button isn't authorisation: the check lives in the shared command layer, because the API and the CLI answer anyone with a valid session."

What’s happening
  1. OWASP's API Security Top 10 (2023) names the three failures: API1 object-level, API3 property-level (excessive data exposure and mass assignment), API5 function-level authorisation.
  2. A field with no entry in the table fails closed: nobody can read or write it until someone decides.
  3. Applying the allowed half of a partly forbidden patch would leave the device in a state nobody asked for.
  4. Where Poly's public guides describe roles they're coarse (every REST command's access level is Admin; directory login maps one AD group per role); finer roles are the general pattern, not Poly's product.
  5. Every change produces an audit record: who, from where, through which interface, old and new values with secrets redacted, and the result.

Learn more: Least privilege and audit logs.

A device's audit log verifies cleanly from entry 1 to 812, but the syslog server's last anchor for it says { seq: 830, hash: "9c1e…" }. What can you conclude?
Show answer

The log was tampered with: entries 813 to 830 existed and were removed, even though what's left verifies on its own. "A hash chain, where each entry's hash covers its contents and the previous entry's hash, proves only that nobody edited an entry without recomputing everything after it. An attacker can re-hash the whole chain, or simply delete the newest entries, which a chain can't detect alone. The anchor is an outside record: every few minutes the device sends its newest sequence number and hash somewhere its users can't edit, so a truncated or rewritten log no longer matches it. What you can't conclude is what the missing entries said, which is why devices ship the full entries off-device too."

What’s happening
  1. In the chapter's model, an edit was caught by the chain; an edit plus re-hashing passed the chain and failed the anchor; deleting the last two entries was caught only by the anchor.
  2. Make tampering evident, not impossible: you can't stop someone with root from editing local storage.
  3. Sign the anchors with a key in the device's hardware-backed keystore so even the receiver can tell a forged one.
  4. Redact secrets before writing (sip.password: "(changed)"), because audit logs are read by more people than the settings page.
  5. Bounded storage, honest about loss: when the ring buffer wraps, record "N entries dropped".

Learn more: Least privilege and audit logs.

How do you make firmware updates secure?
Show answer

Trust comes from a signature the device checks, not from the channel the image arrived on. "The vendor signs a manifest with the product, version, a security version and the image hash, using a key held in an HSM that only the release pipeline can use. The device has the public key in its read-only system image, verifies the signature over the exact bytes before parsing anything, then checks the product, that the security version is at least its stored rollback floor, and the image hash as it streams in. It installs to the inactive A/B slot, reboots, and verified boot checks each stage; if the new version fails its health checks it falls back automatically, and only a healthy boot raises the rollback floor. Every step is audited, and every release ships with an SBOM."

What’s happening
  1. Carriers are untrusted: Poly's public guide lists its own update server, a custom HTTP or HTTPS server on the customer's network, Teams device management, provisioning and a USB stick. The guide doesn't describe how images are verified, so this is the general pattern.
  2. The chapter's model rejected a flipped bit (hash), an edited manifest and a foreign key (signature), another product's image (product), and a genuine old release (rollback floor).
  3. The floor is a separate security version, so customers can still roll back for a functional bug between releases that fix nothing security-relevant.
  4. "It's HTTPS from our own server" isn't a signature: a compromised server serves malware over perfect TLS.
  5. Plan key rotation before you need it: devices can trust current and next keys, and an update can retire the old one.

Learn more: Supply chain and update security.

The release page shows a firmware image's SHA-256 next to the download, and the device checks a hash the admin pastes in. Which attacks does that stop?
Show answer

Accidental corruption and tampering between the site and the admin; not a compromised site, not a downgrade, and not a pasted wrong hash. "The hash and the image come from the same place, so whoever can replace one can replace both. An old genuine release has a genuine hash on its own old page, so it passes. The fix moves trust from what the website says to what the vendor's offline key signed: the hash lives inside a signed manifest, the device holds the public key from the factory, a rollback floor refuses old versions, and the admin no longer types anything security-critical."

What’s happening
  1. A hash compares bytes with bytes: it catches a flaky download or a bad USB stick.
  2. A hash published next to the download on the same server protects against corruption, not against whoever controls the server.
  3. A downgrade attack installs your own old code with its known hole, nearly as good for an attacker as installing theirs.
  4. Signing the manifest (not the multi-gigabyte image) keeps hashing, which can stream, separate from the key operation.
  5. On Android, Verified Boot keeps a rollback index in tamper-evident storage, which is also why recovery after a bad release is usually fix-forward.

Learn more: Supply chain and update security and, for rollback and fix-forward, CI/CD for web UI, APIs and firmware.

What is an SBOM for, and why is it now on a device team's release checklist?
Show answer

It lists every component in a build, so "are we affected by this new CVE, and on how many devices?" becomes a query instead of a week of asking teams. "Each firmware build gets a machine-readable bill of materials, SPDX or CycloneDX, with every component's name, version, supplier, package URL, hashes and licences, generated by CI from what was actually built and kept with the release. Joined with the fleet's version counts, a new advisory turns into 'build 5.0.2 is affected, 1,874 devices need 5.1.0'. And regulation is making it mandatory: the EU's Cyber Resilience Act requires an SBOM covering at least top-level dependencies."

What’s happening
  1. In the chapter's model one advisory (invented for the example) resolved to one affected build in seconds, and the SBOM also showed an unasked-for risk: an old build still shipping an out-of-support OpenSSL branch.
  2. CISA, with the NSA, the FBI and international partners, published "2026 Minimum Elements for a Software Bill of Materials" on 29 July 2026, replacing NTIA's 2021 minimum elements.
  3. The Cyber Resilience Act is Regulation (EU) 2024/2847; per the chapter (checked against the Commission's reporting page and law-firm summaries, not the Official Journal), reporting obligations have applied since 11 September 2026 and most others apply from 11 December 2027.
  4. It only works if every build's SBOM comes from CI, not from a spreadsheet.
  5. Real tooling matches against OSV, NVD and vendor advisories and copes with messier version schemes.

Learn more: Supply chain and update security.

From VideoOS 4.6.2, Poly's REST API wants an X-XSRF-TOKEN header, and 4.7.0 added a switch to turn that off. What does that teach about security fixes in a device API?
Show answer

A security fix is also an API change, and the shape worth copying is secure by default with an explicit compatibility switch. "Login-then-POST scripts that had worked for years started failing until they learned to send the header, so HP added a setting, on by default and recommended to stay on, for systems that can't send the token. That's the honest pattern for a breaking security change: secure default, a documented switch for clients that can't change, a clear recommendation, and release notes that name it. One thing I'd add that the public notes don't mention: log when the switch is used, so you know who still depends on it."

What’s happening
  1. The sources are community knowledge-base posts on HP's support site (the REST API FAQ, 8 January 2026, and the 4.7.0 release post, 18 June 2026), which carry HP's disclaimer that they aren't official documentation.
  2. The 4.7.0 setting is under Security > Access, "Require X-XSRF Authentication Token When Using REST API".
  3. The FAQ's examples send the header on every call, reads included; the public material doesn't say whether reads without it are refused, so don't claim "state-changing calls only".
  4. A compatibility matrix would have shown the break as a red cell for every old script row before release.
  5. It's also a good question to ask in the interview: how was it communicated, and who decided on the escape hatch?

Learn more: Rules for compatible change and Poly video devices and PolyOS in brief.

#Android, SDK and delivery questions

Added

The deck for everything around the API: the Android platform it runs on and the teams who own it, the SDK partners call it through, and how the software is tested, shipped to a fleet and watched once it's there. For each card, check that you named the boundary (a process, a contract, a version number, a ring), what proves it holds (a test, a check, a gate), and what you'd do when it doesn't.

The mental model is a relay race: the dangerous part is the handover zone. The web UI hands a request to the API, the API to a platform service, the SDK to a partner's code, the pipeline to a fleet. Most of the questions here are about practising those handovers before a customer finds the dropped baton.

This group covers where the server lives on Android, the Android 11→13 behaviour changes, Binder and AIDL, working across SDK, platform and web teams, debugging across the stack, then SDK design, errors and retries, SDK versioning and docs, and finally API testing, simulators, hardware-in-the-loop labs, CI/CD with staged rollouts, and flags, telemetry and diagnostics.

Where would you run the admin web server on an Android-based device?
Show answer

In a foreground service inside a privileged system app, or as a native daemon started by init, depending on what it must do and who ships its updates. "Android was designed for phones, where any app can be killed to save memory, so a server that must answer at 3 a.m. has to live somewhere Android treats as 'must stay up'. If it's mostly a front end for framework APIs and we want to ship it as an APK, a foreground service in a system app: preinstalled in priv-app with its privileged permissions allowlisted, signed with the platform key so it holds the platform's signature permissions, and restarted on BOOT_COMPLETED and MY_PACKAGE_REPLACED. If it must be up before the framework, own port 443 directly or survive anything, a native daemon that init restarts, talking to the framework over Binder. Either way the HTTP layer stays thin, and every platform failure maps to a clear status."

What’s happening
  1. Three homes: a regular app with a foreground service lives under every app rule; a privileged system app in priv-app can hold privileged and signature permissions and be persistent, but most framework rules still apply; a native daemon has no targetSdkVersion and is restarted by init.
  2. The public Poly documentation describes the web interface from the admin's side and doesn't say how it's implemented on PolyOS, so treat any assumption about Poly's internals as a question to ask.
  3. A regular app can't bind ports below 1024: on the chapter's Android 16 emulator image ip_unprivileged_port_start was 1024 and binding port 80 as the app's uid gave "Permission denied". The usual fix is a small native proxy with NET_BIND_SERVICE, which is platform-team work.
  4. ServerSocket(8080) listens on every interface the device joins; if a TLS proxy fronts the app, bind the app to loopback so nothing can bypass it.
  5. Ask the platform team early about ports, TLS certificates and which Android rules apply, because those decisions live in the system image.

Learn more: Web UIs and APIs on an Android-based device.

You install a new build of the web API app with adb install -r. Its service returns START_STICKY, yet the API stays down. Why?
Show answer

Installing an update kills the app's process, and START_STICKY doesn't restart a service whose package was replaced. "START_STICKY asks Android to recreate a service killed for memory. After a package replacement nothing in the new version runs until an entry point fires: an activity, a broadcast, a binding. The entry point for this case is the MY_PACKAGE_REPLACED broadcast, sent only to the app that was just updated, and it's one of the documented exemptions from Android 12's ban on starting foreground services from the background, so the receiver may call startForegroundService(). On the chapter's Android 16 emulator, adding it brought the server back within milliseconds of the install."

What’s happening
  1. The same is true for updates delivered by a device's own update system, not just adb.
  2. The boot receiver handles both BOOT_COMPLETED and MY_PACKAGE_REPLACED, and checks the action, because a receiver should only act on what it expects.
  3. A receiver with an intent filter must say android:exported explicitly when targeting 31+; false still receives these system broadcasts.
  4. startForegroundService() is a promise: the service must call startForeground() within about the ANR interval, or the system crashes the process.
  5. The system log says it plainly: Background started FGS: Allowed [... code:PACKAGE_REPLACED ...].

Learn more: Web UIs and APIs on an Android-based device.

How would you approach moving the device apps from the 4.x Android base to Android 13?
Show answer

First confirm the real starting point, then split the work along two dials: the OS version on the device and each app's targetSdkVersion. "HP's public 5.0.0 notes say the second-generation systems go from Android 10 to 13 and the TC10 controller from 11 to 12, while the role description says 11 to 13, so I'd ask which base each device and app is on, because if it's 10, the Android 11 changes are part of the jump. Upgrading the OS brings the 'all apps' changes to every app at once, so those are tested with the apps unchanged. Retargeting each app to 33 brings the target-gated ones: explicit exported, PendingIntent mutability, no background foreground-service starts, package visibility, new Bluetooth and Wi-Fi permissions. Build failures first, then runtime behaviour on the new OS: boot, update, reboot, the service being stopped, permission denial."

What’s happening
  1. The two-dial model: in the chapter's scenarios, the same target-29 APK on Android 13 picked up 7 "all apps" changes; retargeting to 33 switched on 12 more.
  2. Several target-gated changes are hard failures: the manifest merger refuses a component with an intent filter and no android:exported, and a background foreground-service start throws ForegroundServiceStartNotAllowedException.
  3. Android's compatibility framework (adb shell am compat disable <change> <package> on debuggable builds) turns individual changes on and off to isolate a regression.
  4. Stage it: OS first with old targets, then retargeted apps, with telemetry on service restarts and API availability.
  5. Being preinstalled doesn't change your target: some rules exempt system apps explicitly, others name roles like device owner, many say nothing. Verify on the real system image.

Learn more: Android 11 to 13: changes that matter and Migrating a legacy device app to a new OS version.

Name the Android 12 and 13 changes that bite a foreground-service web server, and say which dial each is on.
Show answer

Android 12, for apps targeting 31: no foreground-service starts from the background, and explicit exported. Android 13, for all apps: the Task Manager and the notification permission. "From Android 12 a target-31 app that calls startForegroundService() from the background gets ForegroundServiceStartNotAllowedException, unless the trigger is on the exemption list, such as BOOT_COMPLETED or MY_PACKAGE_REPLACED; being a system app isn't on it by itself. On Android 13, regardless of target, users can stop an app with an ongoing foreground service from the notification drawer, with no callback, so the server can be stopped by a person. And POST_NOTIFICATIONS is a runtime permission, but if it's denied the service keeps running; only its notification disappears from the drawer."

What’s happening
  1. Exemptions named in the docs include a visible activity, a notification or widget tap, the boot and package-replaced broadcasts, and device owners and profile owners.
  2. A restart path triggered by a custom broadcast (the network came up) works on target 29 and throws on target 33, so redesign it before retargeting: keep the service running rather than stopping and restarting it.
  3. Test the Task Manager case with adb shell cmd activity stop-app <package>, and check ApplicationExitInfo for REASON_USER_REQUESTED on the next start. Whether a device's own system UI exposes it to room users is a platform decision the public docs don't describe for PolyOS.
  4. Android 11 (target 30) adds camera and microphone foreground-service types and package visibility; Android 14 (target 34) will require a type on every foreground service.
  5. On the chapter's emulator (Android 16, app targeting 33; not an Android 13 image), after "Don't allow", dumpsys still showed isForeground=true and the API answered.

Learn more: Android 11 to 13: changes that matter.

How does the web API on the device talk to the platform's own services?
Show answer

Through Binder: bound services with AIDL interfaces, protected by a signature permission and checked on every call. "Every HTTP request becomes one or more Binder calls into another process. The platform's service declares a signature permission, so only apps signed with the same key, usually the platform key, can bind, and it calls enforceCallingPermission in every method, because the kernel stamps each call with the caller's uid and that can't be forged. On the client I handle all three outcomes of bindService (connected, false, SecurityException), map a dead or disconnected service to 503 with Retry-After, and rebind in onBindingDied, which is what happens when the platform side is updated."

What’s happening
  1. The office-phone model: you dial an extension (bind), the exchange stamps caller ID (Binder.getCallingUid()), the person answering decides per call, and a dead line is DeadObjectException or onServiceDisconnected.
  2. bindService() returning false is silent: in the chapter's emulator run an invisible package (no <queries>, and no <uses-permission> for the provider's permission) gave false, not an exception, and a client that ignores the return value never connects.
  3. A SecurityException from bindService crashed the whole app in the first version of the client; catch it and answer 503.
  4. After kill -9 of the settings process, Android restarted it in 1 s, then 4 s on the second crash: back-off for services that keep crashing, seen at the HTTP layer as growing gaps of 503s.
  5. Binder calls arrive on a thread pool, possibly concurrently, so the service must be thread-safe; oneway callbacks return at once, so one slow listener can't stall the service.

Learn more: Talking to platform services.

Version 3 of an AIDL interface inserts a new method second in the file. The web API ships v3 while the platform still runs v2. What happens when it calls setSetting?
Show answer

The v2 service runs a different method, and nobody gets an error. "AIDL proxies send a transaction number, FIRST_CALL_TRANSACTION plus the method's position in the file, not its name. Inserting a method shifts every later number by one, so v3's setSetting is code 4, which v2 dispatches as registerListener; on the chapter's Android 16 emulator the settings process logged a Parcel warning, changed nothing, and the client saw false for a valid write and null for a valid read. So interfaces are append-only, never reordered or changed; there's an apiVersion() call clients check before using anything newer; and inside the platform build we'd use Stable AIDL, which freezes versions and makes a missing method throw instead of returning silently."

What’s happening
  1. Both versions have the same interface name, so the descriptor check at the start of the parcel passes.
  2. Appending is safe but silent: an old service receiving an unknown code returns false from onTransact, the generated proxy ignores that, and the caller reads null. That was checked on the emulator too.
  3. Stable AIDL (source.android.com, updated 9 October 2026): interfaces declared with aidl_interface, changes limited to methods at the end and parcelable fields at the end with defaults, and the Java backend throws RemoteException saying the API is not implemented.
  4. Keep one copy of the .aidl files (a shared module or published artefact), not two hand-synced copies.
  5. A normal AIDL call blocks with no timeout of its own: call platform services from worker threads, and add a timeout at the HTTP layer (504), knowing the worker thread is still stuck inside transact().

Learn more: Talking to platform services.

How do you keep the web, device API and platform teams from breaking each other, especially across time zones?
Show answer

Make every seam an owned, versioned contract, and test it from both sides. "The platform team owns the AIDL interface and the settings catalogue; my team owns the OpenAPI document for the device API. Changes start as short written proposals the consuming teams review asynchronously, interfaces are append-only with a version the client checks, and we negotiate at runtime: apiVersion() on the AIDL side, capability discovery on the HTTP side. My team keeps a fake of each platform service so the web UI can be built without a device, and the same contract test suite runs against the fake on every commit and against the real platform build nightly, so behavioural drift shows up as a named failing rule instead of a customer bug."

What’s happening
  1. In the chapter's model the fake passed all five rules and the platform build failed two: an unknown key threw instead of returning null, and an out-of-range value was clamped instead of rejected. Both looked reasonable from inside the platform team; both broke the HTTP layer.
  2. Test failure semantics, not just shapes: what null, false and each exception mean, and which HTTP status each becomes.
  3. Fakes written by the consumer tend to be too kind; give them switches for disconnected, slow, RemoteException and SecurityException.
  4. Integration environments from cheap to real: fakes on a laptop, the Android emulator, an emulator built from the platform tree, lab devices, then a field pilot.
  5. A one-page proposal reviewed asynchronously beats a meeting half the teams attend at midnight.

Learn more: Working across SDK, platform and web workstreams.

The contract says an out-of-range setSetting returns false. The nightly lab run shows the platform build clamping 140 to 100 and returning true, and the platform team says clamping is friendlier. Whose change is it?
Show answer

The platform build is out of contract today, but the next step is a contract decision, not a quick fix on either side. "The test did its job: a named rule failed against one implementation, which turns an argument about whose code is wrong into a question about the written contract. Until it's resolved, the device API mustn't silently accept the new behaviour, because the HTTP API documents 422 for out-of-range values and partners may rely on it. If clamping really is better, it's a contract change: proposed in writing, reviewed by the consuming teams, versioned, and then the fake and the tests change with it."

What’s happening
  1. Never "fix" the fake to match the build: that silently moves the contract with no owner having agreed.
  2. A versioned change could keep rejection for old clients, or report the clamped value back so callers can see what was applied.
  3. This is the same rule as REST compatibility: the meaning of an existing answer doesn't change in place.
  4. As a lead, the useful move is making the decision visible and owned, not winning the argument.
  5. The device API's own behaviour (a 422 becoming a silent 200 with the wrong value) is what partners would experience; that's the cost to put in the proposal.

Learn more: Working across SDK, platform and web workstreams.

An admin says "saving a setting sometimes fails", and the UI briefly shows "device unreachable" a few seconds after saving. How do you debug it?
Show answer

Follow the request down the stack and read each layer's record in time order. "First the admin's side: a HAR or the browser's Network panel gives the status and timing, so I know whether the device answered and with what. Then the device API's access log for that time and request id, then logcat for the API process and for system_server. A few seconds of 'unreachable' with no reboot usually means 503s, not timeouts, and the classic cause is a platform service crashing on the new value: ActivityManager logs the death and 'Scheduling restart of crashed service … in 4000ms', and the back-off grows if it keeps crashing, which matches 'sometimes'. I'd reproduce with curl through adb forward, check state with dumpsys, and toggle a suspected behaviour change with am compat on a debug build."

What’s happening
  1. adb logcat -v threadtime gives pid, tid and millisecond times; filter with --pid or -s Tag, because a device's log is busy.
  2. In the chapter's analysis of real emulator logs, the outages were 1,090 ms and 4,083 ms, bracketed by the web API's own disconnected and connected lines.
  3. The fixes sit in three places: the platform service (validate, don't crash), the device API (503 with Retry-After), the UI (retry before claiming "unreachable").
  4. adb forward skips the device's real network interface, firewall and TLS termination; a bug that disappears through it is often in exactly those parts.
  5. The cheapest investment: a request id generated at the edge and logged around every platform call. Note the clock offset between laptop and device before lining up timelines.

Learn more: Debugging across the stack.

What makes a good SDK, in your view?
Show answer

Ergonomics, a real error model, discoverability and stability, plus being diagnosable. "The common task is one obvious, typed call with sensible defaults, device.updateSettings({ volume: 55 }), with options in an options object so they can grow. Failures are typed classes mapped from the API's problem details, carrying the fields, the request id and whether it's safe to retry, so callers react without parsing strings. The editor teaches the API through types and doc comments, and the examples are tested. And upgrading doesn't break working code: semantic versioning, deprecation before removal, and on a device platform one major that works across the firmware the fleet actually runs. Diagnosable means a logger hook that redacts secrets, because 'what did the SDK send to the room?' is the first question in every support ticket."

What’s happening
  1. An SDK is a phrasebook plus a fixer: it translates the protocol and handles the bureaucracy (login, re-login after a reboot, waiting when the device is busy, paging). A thin wrapper that only renames fetch adds a dependency without removing work.
  2. Microsoft's Azure SDK General Guidelines (checked 27 October 2026) put it as idiomatic, consistent, approachable, diagnosable and dependable.
  3. Conventions that make it feel designed: a small set of verbs, begin… for long-running work, units in option names (timeoutInMs), a constructor that does no I/O, plain data returned.
  4. The opposite failure is an SDK with no way out: offer one documented escape hatch, sendRequest({ method, path, body }), that still goes through auth, timeouts and logging.
  5. For one main language, a hybrid is usually right: generated types underneath, handwritten session handling, retries, errors and paging on top.

Learn more: What makes a good SDK.

Which of these belong in a device SDK, and which in the application: re-login after a reboot, retrying a timed-out dial, a "device busy" toast, following pagination cursors, caching settings for five minutes?
Show answer

SDK: re-login and paging. Application: the dial retry, the toast and the caching policy. "Re-login has exactly one right behaviour: on a 401 because the session vanished, log in once with the stored credentials and repeat the call. Following nextCursor is protocol detail, exposed as an async iterator. Dialling isn't idempotent: if the request reached the device and only the response was lost, a blind retry places a second call, so the SDK reports a timeout that says it may have been applied and lets the application decide. What to show for 'busy', and how stale settings may be, depend on the screen; the SDK makes them easy (typed errors, ETags) without deciding them."

What’s happening
  1. An SDK that pops up UI or writes to console uninvited is a nuisance in a server process.
  2. The paging iterator should be lazy: breaking out of for await after two items fetched one page, not the whole history.
  3. Don't offer toArray() over a paginated list; the Azure guidelines say not to, because it hides an unbounded download.
  4. Caching: return ETags and accept If-None-Match, and let the application choose the lifetime.
  5. Retries live in one layer, usually the SDK, because it knows which calls are idempotent; everything else passes errors through.

Learn more: What makes a good SDK and Errors, retries and timeouts in an SDK.

How would you build a TypeScript client SDK for a device's REST API?
Show answer

Three layers: a typed public surface, one request pipeline every call goes through, and an injectable transport. "The surface is methods like getSettings, updateSettings(patch, options) and listCalls() returning an async iterator, with types generated from the OpenAPI spec and friendlier aliases on top. The pipeline runs in a fixed order: auth, with a lazy single-flight login shared by concurrent calls and one re-login when the device reboots and forgets the session; a per-attempt timeout plus the caller's abort signal; retries only for idempotent calls; problem details mapped to typed errors; debug logging with secrets redacted. The transport is an injected fetch, so the same SDK runs in Node, in the admin UI with cookies, and in tests against a fake device. Every request carries the SDK version."

What’s happening
  1. Single-flight login: in the chapter's model two parallel calls caused one POST /session and both used the same token; without it a dashboard loading ten panels opens ten sessions on a device that allows a handful.
  2. Clear the token only if it's still the one you used (compare-and-set); otherwise a late 401 throws away the fresh session another call just created, and the device sees three logins instead of two.
  3. A second 401 after re-login means the credentials are wrong: throw instead of looping.
  4. The constructor does no I/O: it can't be awaited, and a client that connects in its constructor can't be created before the device is reachable.
  5. The User-Agent (or x-sdk-version in a browser) tells device logs and support which SDK version partners still run.

Learn more: Building a TypeScript client for a device API.

How should an SDK handle errors, retries and timeouts?
Show answer

Typed errors mapped from problem details, retries only for transient failures of calls that are safe to repeat, and a timeout and abort signal on every call. "Errors map by the stable problem type first, then by status, onto a base DeviceApiError with the status, the body and the request id, and subclasses like ValidationError with the bad fields or RateLimitError with the delay. Retries happen in one layer, the SDK, for network errors, timeouts, 408, 429 and 5xx, and only for idempotent methods or a POST with an idempotency key; capped exponential back-off with full jitter, honouring Retry-After, about three attempts, and a way to turn them off. Each attempt gets AbortSignal.timeout, combined with the caller's signal by AbortSignal.any: a timeout may be retried, a caller's cancel never is."

What’s happening
  1. retryable means "the failure is transient", not "retry this call": a 503 on GET /status is retried, a 503 on POST /calls isn't.
  2. The chapter's retry model: GET waited 301 and 448 ms (seeded full jitter), PUT waited exactly the device's Retry-After of 3 s, dial wasn't retried, and a keyed reboot was retried once while the device counted one reboot.
  3. Public guidance agrees on the shape: Google's AIP-194, the Azure Core guidelines (3 retries, 0.8 s exponential base, at most 60 s), and AWS's "Exponential Backoff And Jitter" post naming full jitter.
  4. Retries multiply across layers: UI, SDK and a proxy each making up to four attempts turn one click into 4 × 4 × 4 = 64 requests to a struggling device.
  5. instanceof fails with two copies of the SDK in node_modules; give every error a stable code and export a type guard.

Learn more: Errors, retries and timeouts in an SDK.

A partner's device.dial() times out after 10 seconds, and they ask you to "just make the SDK retry dial on timeouts". What do you say?
Show answer

No blind retry: the call may already be ringing. Make the operation safe to repeat, or make its outcome checkable. "A timeout says nothing about whether the device acted: the request may have been lost on the way in, or the device may have started dialling and the response was lost, in which case a retry puts the room into a second call or gets 'already in a call'. The general fix is an Idempotency-Key on POST /calls: the SDK generates one per dial() and reuses it on retries, and the device returns the original result, so the SDK can then retry safely. Where old firmware can't change, the application reads the active call after a timeout and only dials again if none exists. Meanwhile the error already says 'it may still have been applied'."

What’s happening
  1. This is Google AIP-194's rule: don't automatically retry requests "in which repeated runs would cause unintended state changes".
  2. In the chapter's timeout model, the device's counter showed the timed-out dial had arrived ("dial":1).
  3. Making uncertainty visible is the SDK's job; guessing isn't.
  4. The recovery path after a timeout should be explicit application code, not hidden in a retry loop.
  5. The same reasoning drives the device API design: put idempotency keys on every POST you can't undo.

Learn more: Errors, retries and timeouts in an SDK and Methods, status codes and idempotency for device APIs.

How do you version and release an SDK for devices on many firmware versions?
Show answer

Semantic versioning about the SDK's own surface, a separate published support matrix for firmware, and capability checks at run time. "There are three version numbers: the SDK's, the device API's and the firmware's, and they move independently. Patches fix, minors add, and majors, batched and rare, cover anything that breaks callers, including subtle cases like a new value in a returned union or changed retry defaults. The support matrix says which device API versions and firmware each SDK major supports; at run time the SDK reads the device's capabilities and throws a clear UnsupportedFeatureError instead of a puzzling 404. Deprecation goes in a minor with @deprecated, a once-only warning and a changelog entry; removal in the next major with a migration guide. Releases come from CI with an API-surface diff, tests against each API version, and npm provenance."

What’s happening
  1. Semantic Versioning 2.0.0: MAJOR for incompatible changes, MINOR for backward-compatible additions, PATCH for fixes; 0.y.z means anything may change.
  2. A new value in a returned union breaks strict TypeScript callers' exhaustive switch with TS2322; publish unions as open (KnownMode | (string & {})) from day one if you want to add values freely.
  3. Surface the device's own Deprecation and Sunset headers as warnings, so integrators see a sunset date in their logs during a firmware migration.
  4. Pre-releases go on a next dist-tag; npm ranges like ^2.0.0 skip pre-releases.
  5. npm publish --provenance (npm CLI 9.5.0+) from GitHub Actions, or npm's trusted publishing (npm CLI 11.5.1+), links the package to the commit and CI run that built it.

Learn more: Versioning and releasing an SDK.

Your SDK is at 1.5.0. What comes next for each: (a) a new optional abortSignal option; (b) removing setVolume(), deprecated in 1.5.0; (c) the returned Mode union gains "meet"; (d) dropping support for a Node.js version?
Show answer

(a) 1.6.0, (b) 2.0.0, (c) 2.0.0 unless Mode was documented as open, (d) 2.0.0 by most libraries' policy. "A new optional option is a backward-compatible addition: old calls still compile and work. Removing a method breaks existing code however long it was deprecated, so it waits for the next major. A value added to a type the SDK returns breaks every strict caller with an exhaustive switch, so it's major unless the union was published as open from the start. Semver itself doesn't cover runtime support, but users experience losing their Node version as breaking, so most libraries treat it as major."

What’s happening
  1. Options objects are why (a) is easy: adding a fourth positional parameter would not be.
  2. (b) is the semver FAQ's two-step deprecation: deprecate in a minor, remove in a later major.
  3. (c) is the TypeScript-specific trap: for an input type, accepting a new value would be minor.
  4. (d) belongs in the support matrix alongside firmware ranges.
  5. Batch the breaking changes into one planned major and ship compatible minors in between.

Learn more: Versioning and releasing an SDK.

A partner says the README's example fails with TypeError: device.setVolume is not a function, though nobody touched the README in months. What happened, and what would have caught it?
Show answer

The SDK changed under the README: 2.0.0 removed setVolume(), and the example was never run against the release. "Untested examples rot silently whenever the API changes, and the drift is invisible until a user copies the code. Docs are part of the product, so they get tests: pull every example out of the README, run it against a device simulator and the release candidate, compare what it prints with the documented values, and block the release on a mismatch. And the 2.0.0 migration guide should have listed setVolume(level) → updateSettings({ volume: level })."

What’s happening
  1. Developers arrive with a task, copy an example, then live in their editor, so the example, the type signature and the error message matter most, in that order.
  2. Diátaxis gives four kinds of documentation: tutorials, how-to guides, reference (generated from types and doc comments) and explanation. Most bad SDK docs are reference pretending to be all four.
  3. A good example runs as written, handles the error that will actually happen, cleans up sessions, says which firmware it needs, and works against a simulator without hardware.
  4. Error messages are documentation: what happened, why, what to do, a stable code, and a link (a problem type URI can resolve to that page).
  5. Publish docs per major and keep old ones online while that major is supported; a partner stuck on 1.x because of old firmware deserves matching docs.

Learn more: Documentation and examples developers actually use.

How would you test a device's REST API?
Show answer

Three levels in CI: unit for logic, integration through the real HTTP stack, and contract against the published spec. "Unit tests for what's easy to get wrong: validation, merge rules, state transitions. Integration tests that run the real Express app in process with supertest, sending exactly the headers real clients send, because wiring bugs like a body parser ignoring application/merge-patch+json return 200 for changes that never happened and no unit test sees them. Then contract tests: every response is checked against the OpenAPI spec (status documented, content type documented, body matches), negative tests are generated from the schema's limits, and the spec itself gets a breaking-change diff on every PR. On a device platform the contract layer matters most, because partners and customers' scripts are consumers we don't control."

What’s happening
  1. The chapter's merge-patch bug: body-parser 2.x's json() defaults to application/json only, leaves req.body undefined for other types, validation finds nothing wrong, and the device answers 200 with unchanged settings. One line fixes it.
  2. The contract test's oracle is a document the developer didn't edit: a rename to volumeLevel and an undocumented 409 failed it, while the developer's own updated tests stayed green.
  3. Fourteen invalid patches generated from the schema found three the hand-written checks accepted, including volume: 0.5, which a device would store and meet again on every boot.
  4. Mocks hand-written by the UI team can agree with the UI's bugs; generate them from the spec and run the same contract check against the mock.
  5. Above that: fewer end-to-end tests on a simulator, and nightly runs on real hardware for what only hardware shows.

Learn more: Testing APIs: unit, integration and contract.

Name the cheapest test that reliably catches each: (a) PATCH accepts brightness: -5; (b) the 404 handler returns HTML instead of problem details; (c) a developer changes uptimeS to a string and updates their own tests; (d) the UI says "Saved" but the display doesn't change until a reboot.
Show answer

(a) unit, (b) integration, (c) contract, (d) end-to-end on a simulator, then hardware in the loop. "(a) is pure validator logic, caught in microseconds with boundary values. (b) is wiring in the error-handling middleware, only visible through the real app, so a supertest request to an unknown path that asserts application/problem+json. (c) is drift the developer's own tests were edited to accept, so only an independent oracle, the published spec saying integer, catches it. (d) is behaviour across UI, API and device: a simulator that models applied versus pending catches the logic in CI, and only real hardware proves the display driver applies it."

What’s happening
  1. Unit tests can't see configuration: parsers, middleware order, body limits.
  2. At least one integration test per endpoint should send the headers real clients send, copied from the web UI's network tab or the SDK.
  3. Breaking-change detection on the spec catches the developer who edits the spec too.
  4. Pick the cheapest level that reliably catches the bug, and keep the expensive levels for what only they can prove.
  5. The test ladder: unit, integration, contract, end-to-end on a simulator or emulator, hardware-in-the-loop; more real and slower going up.

Learn more: Testing APIs: unit, integration and contract.

How do you let a distributed team develop and test against hardware they don't have?
Show answer

A device simulator built as a first-class product of the team, kept honest by parity checks against real devices. "It speaks the same API, generated from or checked against the spec, and has the device's real state machine for settings, reboots and updates, with a fake clock so a 45-second reboot is instant in tests and fault controls (power loss, slow network, a failed update) on a separate control surface. Every developer runs it locally, CI runs UI and API end-to-end tests on it, and the cloud team can start hundreds. A nightly lab job records real device responses and a parity check fails CI when the simulator drifts. For the Android layer, the Android Emulator or Cuttlefish at the right API levels; for camera, codecs and power loss mid-flash, the lab."

What’s happening
  1. The flight-simulator model: the controls are real, the bad days are scriptable and repeatable.
  2. While "down", the chapter's simulator destroys the TCP socket instead of answering, so clients see ECONNRESET, which is what a rebooting device really gives you.
  3. Its parity check found three drifts: uptimeS as a non-integer, a missing peripherals array and a missing Retry-After on reboot.
  4. A simulator that never fails trains everyone's code to expect perfection; give it measured defaults and run part of the suite with faults on.
  5. Drive normal state changes through the real API; use control endpoints only for faults the world would cause.

Learn more: Device simulators and emulators.

What would you test on real hardware, and how would you run a device lab?
Show answer

Only what hardware can show, as matrices, on rigs run as a shared service. "Power cuts at every step of an update, with good and bad builds, so we prove both survival and rollback; peripherals in every plug order; the actual HDMI and audio output; long soak tests for memory and heat; real network impairment; and the update path from every version still in the field. Each rig has switchable power, a serial console, adb and the device API, a switchable USB hub and HDMI capture, driven by a harness like Tradefed or LAVA. Every job starts by flashing and resetting the device, rigs are health-checked and quarantined automatically, jobs are scheduled by capability, failures keep every log and capture, and the lab is remotely bookable, so the India team gets real hardware without shipping devices."

What’s happening
  1. The chapter's power-cut matrix: an in-place updater bricked at "write 50%"; A/B survived every cut and rolled back a bad v2; an A/B updater that marked the new slot successful too early survived every power cut and boot-looped on a bad build.
  2. That third updater would pass a test plan that only cuts power: the value is in the matrix, every cut point × good and bad builds.
  3. Android's A/B scheme (source.android.com): two slots, a bootable flag, and a new slot that only counts as successful after its first boot passes checks.
  4. Tier the cadence: simulator tests per PR, a HIL smoke suite per merge, the full matrix nightly and before releases.
  5. HIL tests that only read the device's own API ("the setting was applied") prove little; check the physical result, a captured frame or a measured audio level.

Learn more: Hardware-in-the-loop and lab testing.

After a nightly lab run, one test fails on every rig, two fail only on rig 3, and one fails and then passes on re-run on two rigs. What do you do with each?
Show answer

Every rig: a regression, block the release. Only rig 3: probably the rig, quarantine and health-check it. Passes on re-run: a flaky test or an intermittent bug, file it and track its flake rate. "A regression fails everywhere, a bad rig fails tests that pass on every other rig, and a flaky test fails and then passes on the same rig. The approach is a limited number of re-runs, then the pattern across rigs. I wouldn't hide the flaky one behind automatic re-runs forever, because it might be a real race, in camera enumeration for example."

What’s happening
  1. Exclude tests that fail on every rig before counting failures per rig, or a regression makes every rig look bad.
  2. Quarantine before anyone debugs the firmware: a loose power cable or a failing display explains a rig-only failure.
  3. Limit re-runs (the chapter's model used two), so a broken build doesn't occupy the lab all night; its triage cost 35 device runs for 20 test-rig pairs.
  4. Keep an inventory of each rig's exact components and firmware, and record it with every result: rigs drift apart.
  5. A failure that can't be investigated the next morning will be dismissed as flaky, so keep serial logs, logcat, bug reports and captures.

Learn more: Hardware-in-the-loop and lab testing.

How would you design CI/CD and rollout for device software?
Show answer

Build every artefact once, prove it, sign it, and promote the same bytes to the fleet in rings with an automatic health gate. "Pull requests run lint, unit, integration and contract tests, a breaking-change diff of the spec, and end-to-end tests of the built UI on the simulator; merges get a hardware smoke suite; nightly, the full lab matrix. Packaging produces a hashed artefact with a provenance attestation, and release signing happens in an HSM-backed service, never with keys in CI. Rollout uses stable buckets from a hash of release id and serial: 1%, 5%, 20%, 50%, 100%, with soak time, installs only in customers' maintenance windows and never during a call. After each stage a gate compares updated devices with a control group, overall and per model, because a bug on one model is diluted in the aggregate. Halting is cheap and tested; recovery is usually fix-forward."

rq-canary.mjs
// Why a fleet-wide dashboard hides a bad release.
// 1) The SRE workbook's example: a 5% canary where 20% of requests fail; the rest of the service is healthy.
const canaryShare = 0.05, canaryFail = 0.2;
console.log(`whole service: ${(canaryShare * canaryFail * 100).toFixed(1)}% failing; canary alone: ${canaryFail * 100}%`);
// 2) A bug on one model (the testing chapter's rollout model): touch panels are about 10% of the updated
//    devices and fail 4%; everything else fails at the background rate of about 0.4%.
const share = 0.1, bad = 0.04, normal = 0.004;
const overall = share * bad + (1 - share) * normal;
console.log(`updated devices overall: ${(overall * 100).toFixed(2)}% against a 0.40% control; touch panels alone: ${bad * 100}%`);
console.log(`overall excess: ${((overall - normal) * 100).toFixed(2)} points; touch-panel excess: ${((bad - normal) * 100).toFixed(2)} points`);
Result
whole service: 1.0% failing; canary alone: 20%
updated devices overall: 0.76% against a 0.40% control; touch panels alone: 4%
overall excess: 0.36 points; touch-panel excess: 3.60 points
What’s happening
  1. Google's SRE workbook ("Canarying Releases") makes the first point: 20% failures in a 5% canary is only 0.05 × 0.2 = 1% of the whole service, easy to explain away as noise. Compare the canary with the control instead.
  2. The second line is the chapter's touch-panel release: 0.1 × 4% + 0.9 × 0.4% = 0.76% overall, an excess of 0.36 points, under that model's illustrative 0.5-point threshold, so an overall-only gate would have let it reach every touch panel.
  3. Per model, the excess is 3.6 points; in the chapter's run the per-model check halted 5.0.3 on day 3 with 2,079 devices updated and 37,921 never exposed.
  4. Stable buckets mean every device in an earlier stage is in every later one, and including the release id gives each release a different first 1%.
  5. Customers of room devices expect this: Microsoft documents Teams Rooms Pro update rings with default durations and updates only in a nightly 12 AM–5 AM window, postponed while a device is busy.

Learn more: CI/CD for web UI, APIs and firmware.

How do feature flags work differently on devices than in a web app?
Show answer

They're evaluated locally from a cached, signed config with compiled-in defaults, targeted by hardware and firmware, and they can only switch between code that already shipped. "A device has to work offline and at boot, before the network is up, so every flag has a default in the firmware and the evaluator never throws: it always returns a value and a reason, OpenFeature-style, which is what lets you debug a flag from telemetry. Rules match model, firmware version, tenant and a stable hash of the serial for percentage rollouts, so raising 10% to 25% only ever adds devices. Kill switches for risky features, expiry dates so flags don't pile up, and one rule I'd hold firmly: flags don't change the API's shape, or two devices on the same firmware answer differently and partners can't rely on the spec."

What’s happening
  1. OpenFeature's requirement 1.4.10: evaluation methods "MUST NOT throw exceptions", and on abnormal execution return the default; reasons such as TARGETING_MATCH, SPLIT, DISABLED, and error codes like FLAG_NOT_FOUND or TARGETING_KEY_MISSING.
  2. Firmware targeting matters: a mode that relies on a driver fix in 5.0.3 must never turn on for 5.0.2.
  3. A new device on first boot with no network evaluates from the factory config and records the config version with each reason, then switches once version 42 is downloaded and verified.
  4. Pete Hodgson's four kinds (release, experiment, ops, permissioning) have different lifetimes; on devices clean-up spans releases, because a flag can't be removed until every device has code that no longer reads it.
  5. If a gated field must appear in a response, advertise it through capability discovery so clients can ask.

Learn more: Feature flags, telemetry and field diagnostics.

How do you collect telemetry and diagnostics from devices you can't touch, without hurting them or the customer?
Show answer

Telemetry buffered on the device and limited to an allowlist; diagnostics on demand, redacted before upload. "Fleet health (crashes, unexpected reboots, update outcomes, call quality) tagged with model and firmware, never with meeting ids or user names, buffered in bounded storage with sequence numbers and a drop counter, so after a weekend offline the collector can tell 'the device dropped data' from 'the pipeline lost it'. For one device, an on-demand diagnostics bundle as a long-running operation, redacted on the device so the support system never holds the secrets, temporary verbose logging that expires by itself, and remote checks behind an admin role with audit logging and correlation ids that tie a click in the web UI to the device's logs."

What’s happening
  1. In the chapter's buffer model, a three-hour outage filled the 120-event buffer, dropped the oldest 75 and counted them; the collector saw the gap 46–120 and the device's own count explained it exactly.
  2. Keep the newest for health data, send in batches when the uplink returns, and back off with jitter so ten thousand devices don't flush in the same second.
  3. Redaction: passwords, tokens and emails replaced; meeting titles removed (they can be confidential in themselves); client IPs pseudonymised with a keyed HMAC, because an unkeyed hash of an IPv4 address is trivially reversible.
  4. Regex redaction is a denylist that misses things; the stronger fix is structured logging with sensitive fields marked at the call site.
  5. Privacy and consent are product requirements enterprise security teams ask about: document what's collected and enforce it in code.

Learn more: Feature flags, telemetry and field diagnostics.

#Questions about this role

Added

The last group is about the job itself: what device platform work is, what the public record says about Poly's devices and the 4.x → 5.x move, how you'd run that migration, and the questions about you. The product facts here are only the public ones the chapters cite, with their sources; where the public material doesn't say (how PolyOS is built inside, the team's plans, anything about "Project Trinity", which appears in no public source the chapters could find), the right answer is to say so and ask.

The mental model for this group is you're the translator: between the platform team's services and release trains, the web team's components and HTTP, partners' "my script broke", and a team in India that needs clear contracts and fast answers across a time-zone gap. Every claim in that story has to be something you've actually done.

So the cards that ask about your experience ("tell me about a time…") don't contain answers. They give the structure, what the interviewer is listening for, and where to prepare your own story: the Tech Lead site's story prompts, written up in its My notes page, or in this site's My notes. Anything you can't back with a real example, leave out.

What is device platform engineering, and how is it different from building a web app?
Show answer

Building the software that lets a fleet of hardware be configured, controlled, monitored and updated, on a runtime you don't own. "The on-device web UI, the REST and command-line APIs, the connection to cloud management and the integration with the OS. The big difference from a web app: the code runs on thousands of devices on customers' networks, many versions are live at once for years, updates are pulled or pushed through admins and partners on their schedule, and some upgrades are one-way. So I design APIs to stay compatible across versions, make clients discover capabilities instead of assuming them, stage rollouts with health gates, and build diagnostics in, because I can't log in to the device when something goes wrong."

What’s happening
  1. The restaurant versus meal-kit model: a web app serves everyone in your building; a fleet ships boxes to kitchens you never see, each cooking whatever version it last received.
  2. Updates aren't always automatic: Poly's parameter guide (4.1.0, November 2023) lists upgrade.auto.enable defaulting to False for Teams and Zoom, True for other providers, so a Teams or Zoom room waits for an admin.
  3. Partners sit in the release path: Microsoft says OEMs provide Android and firmware updates for Teams devices to Microsoft for certification, distributed through the Teams Admin Center.
  4. Lifetimes are long: HP's newsroom (3 February 2026) says VideoOS 5.0 establishes the foundation for future updates to Android 17 and support through 2032.
  5. "Down" means a meeting room that can't start a call, not a dashboard error rate; "it works in the lab" isn't "it works in the field".

Learn more: What device platform engineering is.

What do you know about our products and platform?
Show answer

What the public documentation says, and where it stops. "The room systems are the Studio X video bars (X30, X50, X70 and the second-generation X32, X52, X72), the modular G7500 and Studio G62, with TC8 and TC10 touch controllers. They run Poly VideoOS, which HP's community posts and your role description call PolyOS; it's Android-based. VideoOS 5.0.0, released in May 2026, supports only the G62, X32, X52 and X72, needs 4.6.0 first, can't be downgraded, and launched for Microsoft Teams and Device Mode, while 4.7 continues for every provider and the first-generation systems. The same hardware runs Poly Video mode or a partner's app, it's managed through the web interface, Poly Lens, provisioning or the partner's console, and integrators use the REST API and a command-line API over SSH or Telnet. What the public docs don't show is how those interfaces are built inside the device, which is the part I'd like to hear about."

What’s happening
  1. Source for 5.0.0: HP's official "Poly VideoOS Software Release Notes 5.X" on HP Support (read 27 October 2026), which also says the systems move from Android 10 to Android 13 and the TC10 from Android 11 to 12.
  2. HP completed its acquisition of Poly on 29 August 2022; documentation has moved to HP Support and newer material says "HP Poly".
  3. The REST API reference (document P55757-001A, August 2025, for VideoOS 4.4.0) uses /rest/ paths with no version segment, sessions from POST /rest/session, a 10 MB request limit and no documented push channel; from 4.6.2 an XSRF token, per HP's community FAQ.
  4. HP's community knowledge-base posts carry HP's disclaimer that they're the authors' opinions, not official documentation; say "a community post on HP's support site" when you quote one.
  5. End with a question rather than a guess: it shows the research and invites the interviewer to fill in what the public material leaves out.

Learn more: Poly video devices and PolyOS in brief.

The role description says PolyOS moves from Android 11 to 13; HP's public 5.0.0 notes say the Gen2 systems go from Android 10 to 13. How do you handle that in the interview?
Show answer

Don't correct the interviewer from a release note, and don't assert either version: ask. "Something like: 'The public 5.0 notes say the second-generation bars go from Android 10 to 13, and the TC10 controller from 11 to 12. Which baseline is this project's 4.x work on?' It shows I've read the notes and that I care about the exact starting point of a migration, because it changes the work: if the base is Android 10, the Android 11 behaviour changes, like package visibility and the camera and microphone foreground-service types, are part of the jump, not background."

What’s happening
  1. The public sources don't explain the difference: it could refer to a different product, an internal baseline, or loose wording. Saying so is the honest position.
  2. The product name follows the same rule: HP's official notes say "Poly VideoOS", its community posts and the role description say "PolyOS"; treat them as one product line and use the interviewer's word.
  3. The same goes for anything internal, such as a project name: if it isn't in a public source, don't present a guess about it as fact.
  4. Have the follow-up ready: the two-dial way to plan the Android work (OS version first with old targets, then retargeting each app).
  5. Questions like this are scored as judgement: "the public docs don't say; here's how I'd find out" beats bluffing.

Learn more: Poly video devices and PolyOS in brief and Android 11 to 13: changes that matter.

How would you approach migrating the web UI and APIs from PolyOS 4.x to 5.x?
Show answer

Inventory, fixtures and a parity check first; settings migrations and contract tests next; then rings with health gates for an upgrade that can't be rolled back. "I'd start with an inventory of the whole 4.x surface: every REST endpoint, CLI command, setting, default and event, including undocumented behaviour integrations rely on, and record real request-response pairs as fixtures. Parity is then measurable: replay them against the 5.x build in CI and classify differences by shape, so added fields pass and removed fields, type changes and status changes become a burn-down list. Settings written by 4.x need numbered, idempotent migrations that keep unknown keys. The contracts with the platform team go under contract tests. And the rollout follows the shape the public notes show: a bridge release, the new OS on one mode first while 4.x keeps getting sustaining releases, every ring gated on health signals with soak time, and a fix-forward pipeline, because there's no rollback."

What’s happening
  1. The chapter's parity check classified five recorded calls: an added field (compatible), volume turned into a string, ssid removed, reboot changed from 200 to 202, and a missing logs endpoint: four decisions before shipping.
  2. Hyrum's law: with enough users, every observable behaviour is depended on, including undocumented endpoints such as the GET /rest/system/bluetooth call HP's community FAQ warns could change.
  3. Migrations are safe to re-run after a crash mid-boot, and a key no step mentions (say a partner app's room id) survives.
  4. In the rollout model, the lab ring passed a build that lost peripherals in 5% of real rooms, because lab rigs carry current accessories; the internal ring halted it at 200 devices instead of 8,000.
  5. Management connectivity is part of parity: 5.0.1 fixed devices showing offline in Poly Lens after upgrading.

Learn more: Migrating a legacy device app to a new OS version.

HP requires every device to be on 4.6.0 before it can take 5.0.0. Why would a platform team impose a bridge release?
Show answer

It shrinks the problem from "every 4.x version to 5.0" to "4.6 to 5.0": one source format, one upgrader, one tested path. "Without a bridge, the 5.0 installer and its data migrations would have to handle every older layout of settings, partitions and partner data still in the field, and the test matrix would include every one of those starting points. With one, 4.6.0 can do preparatory work that's easier on the old OS: migrate settings, check space, update the update agent, refuse a mode 5.0 doesn't support. HP's own wording names the reason: skipping 4.6.0 is 'an untested upgrade path that may result in your system not functioning correctly'. The cost is time: devices far behind take two updates."

What’s happening
  1. The release notes add that a system on an earlier version automatically loads 4.6.0 first.
  2. "Untested" is the key word: the bridge exists so the tested path is the only path.
  3. For a one-way upgrade, where a failure can't be rolled back, that trade is usually worth it.
  4. The same idea exists on the web side as a minimum supported version for a migration script.
  5. The bridge itself has to be rolled out first, which is part of the schedule.

Learn more: Migrating a legacy device app to a new OS version.

A colleague says: "If 5.0.0 misbehaves in a customer's Teams rooms, Lens can push 4.6.2 back to them." What's wrong with that plan?
Show answer

The public notes say a 5.x system can't go back to 4.x, and even a factory restore lands on 5.0.0, so recovery has to be forward. "'Once you update your system to Poly VideoOS 5.0.0, you cannot downgrade', and the release 'also updates the system's factory restore partition'. The notes do describe a manual downgrade in Lens by choosing a previous version, but read together with that line, my reading is that it moves between 5.x builds, never back to 4.x; the notes don't spell that out. So every gate has to be passed before a device upgrades: longer soak in early rings, health checks that block promotion, and a pipeline that can ship a 5.0.x fix quickly, which is what 5.0.1 in August 2026 looks like in public."

What’s happening
  1. Say "my reading" when you combine two statements from a source; it's an inference, and the interviewer may know the real answer.
  2. It changes the API work too: 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.
  3. On Android devices there's also a general mechanism: Android Verified Boot keeps a rollback index and refuses older images once a build has been marked good, so going back usually means shipping a newer build. HP's notes don't say whether that's why 5.x can't return to 4.x, so don't present it as their reason.
  4. The release plan turns around: the expensive testing happens before the one-way door, not after.
  5. Halting a rollout stays cheap; undoing one isn't possible.

Learn more: Poly video devices and PolyOS in brief and CI/CD for web UI, APIs and firmware.

Why might 5.0 have launched for Teams and Device Mode first? How would you raise that?
Show answer

One plausible reason is partner certification dates, but that's an inference, so raise it as a question. "Microsoft's table for Teams devices on Android lists a certification end date of 3 September 2026 for Android 10 and 11, already extended once, and 15 August 2028 for Android 12 and 13. A platform whose Teams rooms run Android 10 has a strong reason to get Teams onto 13 first. HP's notes don't connect 5.0's Teams-first scope to those dates, so I'd say what's public and ask: 'How do you decide which provider modes come next, and what does parity mean for the web UI and API in each?'"

What’s happening
  1. Source: Microsoft Learn, "Android versioning support for Teams devices", last updated 28 September 2026; support for Android 10 and 11 ends 3 September 2027, for 12 and 13 on 15 August 2030.
  2. Microsoft's certified-devices page lists the Studio X32, X52, X72 and G62 on Android 13 with firmware 5.0.0, certified until 15 August 2028.
  3. Other providers were moving too: Poly's Lens blog (3 March 2026) announced Zoom Rooms re-certification on Android 13, while the 5.0 compatibility guide (March 2026) still marked Zoom "Not Supported", so Zoom support was evidently coming in a later 5.x release; the public documents don't say which.
  4. Partners are part of the system: their certification rules and update channels shape the release plan.
  5. Asking it well shows you think about why the work exists, not only how to do it.

Learn more: Poly video devices and PolyOS in brief and Room systems and provider modes.

A partner asks: "Release 4.6 adds a new endpoint. Can our cloud service call it on every customer device from next month?"
Show answer

No: a month after release only part of the fleet will have it, so their code must check per device and handle "not supported". "In the chapter's adoption model about 53% of the fleet had the new release after four weeks and 18% still didn't after twelve, because Teams and Zoom rooms wait for an admin, devices are switched off, and staged rollouts take weeks. So the answer is: '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 watch adoption grow.' If they treat a 404 from old firmware as an outage, they'll page themselves for a version mismatch."

What’s happening
  1. The release reaching the first devices isn't the release reaching the fleet: the curve rises fast with auto-updaters, then flattens into a long tail.
  2. Capability discovery is the design: read a capabilities document and call the endpoint only where it exists.
  3. Unknown devices fall back to the old behaviour.
  4. Some rooms only update when someone schedules it, and some never do until a support window forces it.
  5. Hearing this question is also a hint about the role: partners will ask it every release, so the capability flag belongs in the API design, not in a support answer.

Learn more: What device platform engineering is and Capability discovery.

How would you design the admin web UI for a device that can run in different provider modes?
Show answer

Start from ownership: for every setting, decide whether the device platform owns it in every mode or only in one, and put that in one registry the UI, the REST API and provisioning all use. "Network, peripherals, security, updates and the provider choice itself belong to the device in every mode; SIP and H.323 call settings belong to the native video mode, because in Teams or Zoom mode the partner app and its cloud own calling. Settings that don't apply are shown read-only with 'managed by Microsoft Teams Rooms', not hidden, and a write that conflicts with the mode gets a 409 with problem details. Changing the provider is a restart-required change with validation up front: is it available for this model and software version, does it support the current network, is there a licence. And the dashboard says which mode the room is in and who manages what."

What’s happening
  1. The games-console model: the console (hardware plus Poly VideoOS) owns network, controllers, security, updates and settings; the game (Teams Rooms, Zoom Rooms, Meet, Poly's own app) owns the meeting experience and its own cloud.
  2. Public facts the UI must respect (Partner Mode Administrator Guide 4.7.0): only Zoom Rooms and Microsoft Teams support Wi-Fi as the primary network; Dialpad and Google Meet aren't supported on the G7500; the Studio G62 is certified for Teams Rooms, Zoom Rooms and Tencent only.
  3. Provider support depends on the software line: the VideoOS 5.0 compatibility guide lists only Teams Rooms and Device Mode, so derive the list from the installed software or a capabilities endpoint, never hard-code it per model.
  4. Changing the provider is General Settings > Provider, Save, then Restart; settings configured earlier are kept and the Zero Touch Onboarding profile is updated.
  5. An admin who wants a Teams room to register to their SIP server is told which layer owns calling in that mode, and pointed to Teams' interop route, such as Cloud Video Interop.

Learn more: Room systems and provider modes.

You've mostly built web apps. Why should we trust you with device APIs?
Show answer

Structure: the hard parts of device APIs are API problems under stricter rules; then one real example; then an honest boundary. "Something like: 'An API on a device is used by many firmware versions at once, by partners' scripts I'll never see, for years, so I design for compatibility first: additive changes, tolerant readers, capability flags, deprecations with dates, and recorded fixtures that prove the new build answers like the old one.' Then a real example of keeping an API or client compatible, from your own work, in two sentences. Then the boundary: you build Android apps, so you know the app-level platform; system services, platform signing and OTA pipelines are the platform team's expertise, and your job is a clean contract with them and tests on both sides."

What’s happening
  1. The middle sentence is for your own example of keeping an API or client compatible: if you can't back it with something you actually did, leave it out rather than fill it with this page's wording.
  2. What the interviewer is listening for: compatibility thinking without prompting, a concrete decision with its reason, and whether you know where your knowledge ends.
  3. Interviewers who work on the OS will ask one follow-up about Binder, SELinux or OTA partitions and find the edge in seconds; draw the boundary yourself first.
  4. These study sites are preparation, not experience: once you've run and changed the models, "I've worked through small working models of…" is true; "at work I…" isn't.
  5. Close on the UI too: never show "saved" until the device has applied it, and design every screen for the device rebooting in the middle.

Learn more: Talking about this role in an interview.

"Why are you a fit for this role?"
Show answer

A six-line skeleton you fill with your own facts, not an answer to memorise. "One line on who you are: full stack, your years, React, Angular, Node and databases, and you build Android apps. The bridge to devices: a real project where the runtime, the client or the server was something you couldn't fully control, and what made it hard. A decision you made, against which alternative, why, and the result. The Android side: what you've really built, and what you know only at the design level. Leading people: a real example of mentoring, reviewing or leading, or say honestly that it was informal. Why this role: owning the web UI and API layer on a platform where compatibility matters, and the 4.x to 5.x migration is exactly that kind of problem."

What’s happening
  1. Line 1 is ten seconds: it sets the frame, not the CV. Round years down if unsure.
  2. Line 2 is the most important and has to be real. Device work is about not controlling the runtime, so pick something where you didn't: an app on users' phones, a client you kept compatible, a service other teams depended on.
  3. 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. That example is the shape, not your story.
  4. The chapter lists prompts from your own repositories (your update manifests on RohitDownloads, Gesture Control's watch helper and relay, Clipboard's accessibility service, your Android CI/CD notes); each is a prompt to describe your part in your own words, not an answer.
  5. Write the filled-in version in My notes and rehearse it out loud; it should take under two minutes.

Learn more: Talking about this role in an interview and the Tech Lead site's story prompts.

"Tell me about a time you had to change an API that other people depended on."
Show answer

Use STAR with the camera on the action, and make the compatibility decision the centre of the story. "Headline in one sentence, result first if it's strong. Situation and task in about thirty seconds: the API, who depended on it, why it had to change. Action in about a minute: three to five things you did, each with a 'because': how you found out who used what, how you kept old clients working (an added field, a version, a flag), how you told people, how you knew when it was safe to remove the old thing. Result with a number if you have one, and how you know. Then what you'd do differently. Prepare for 'why not just version it?' and 'what if a client never migrated?'"

What’s happening
  1. What the interviewer listens for: that you found the consumers before changing anything, chose an additive path, communicated with dates, and measured usage before removing.
  2. Weak answers spend two minutes on the project's history and then say "we did some things"; the action should be about half the time.
  3. Say "I" for what you did, and one clause for what others did.
  4. If your real story is small (an internal API, two consumers), tell it honestly; the reasoning is what scores, not the size.
  5. The story itself must come from your work: write it in My notes from the Tech Lead site's story prompts. This card deliberately has no sample answer.

Learn more: the Tech Lead site's The STAR method and its variants, and Rules for compatible change for the technical half.

"Tell me about a time something you shipped broke for users, and what you did."
Show answer

Ownership, containment, fix, prevention, in that order, and keep the blame out of it. "Situation and task briefly: what shipped, how you found out it was broken, how many people it affected. Action: what you did first to stop it getting worse (halt, roll back or fix forward, and why that choice), how you found the cause, how you kept the people affected informed, and what you changed so it couldn't happen the same way again: a test, a gate, a check in the pipeline. Result: how quickly it was contained and fixed, and whether the prevention has held. Learning: the one practice you've kept since."

What’s happening
  1. What the interviewer listens for: that you own your part without excuses, that you contain before you investigate, and that the prevention is systemic (a test, a gate), not "be more careful".
  2. For this role, connect it to devices if your story allows: why "fix forward" when you can't roll back, why a health gate would have caught it earlier.
  3. Avoid blaming another team; describe what you did at the seam.
  4. Have a number for impact or time to recover if you know it honestly; "about" is fine.
  5. Prepare the story from your own work in My notes; this card has no sample answer.

Learn more: the Tech Lead site's Questions for a lead full stack role, and CI/CD for web UI, APIs and firmware for the technical half.

"Tell me about a time you disagreed with another team about an interface or a decision."
Show answer

Show that you moved the disagreement onto evidence and a written decision, and kept the relationship. "Situation and task: which teams, what the interface was, what each side wanted and why both views were reasonable. Action: how you made the disagreement concrete (a test that failed, data, a written proposal with options), who decided and how, and what you did to keep working together while it was open. Result: what was decided, whether it held, and what it did for the next disagreement. Prepare for 'what if they'd refused?' and 'what would you do differently?'"

What’s happening
  1. What the interviewer listens for: curiosity about the other side's reasons, a decision recorded with an owner, no "I was right and they eventually saw it".
  2. A device-platform shape you can recognise in your own experience, if you have one like it: a contract test fails because the other team changed behaviour they thought was friendlier, and the answer is a contract decision, not a quick fix on either side.
  3. Across time zones, written proposals reviewed asynchronously are themselves part of a good answer.
  4. Disagreeing and committing to the outcome is a strength to show, not a weakness to hide.
  5. Your own story goes in My notes; the card gives only the structure.

Learn more: Working across SDK, platform and web workstreams and the Tech Lead site's story prompts.

"How would you make a team of developers in India productive on this work, from the US?"
Show answer

Contracts and mocks so nobody waits, reviews and decisions that work asynchronously, and the overlap hours used for unblocking. "Design-first API changes as spec pull requests a developer can propose at the end of their day and get reviewed overnight; a device simulator and fakes of the platform services so they can build and test without hardware or without waiting on another team; a remotely bookable lab for what only hardware shows; small pull requests with reviews that teach; decisions written down. And the honest part: if you've led or mentored people across time zones, give the real example; if your experience is informal, say so and describe what you'd do in the first weeks."

What’s happening
  1. What the interviewer listens for: that you remove waiting (the most expensive thing across a time-zone gap), that you delegate ownership rather than tasks, and that quality is consistent across locations.
  2. The technical chapters supply the mechanisms: design-first review across time zones, simulators and fakes, contract tests against both, the lab as a service.
  3. The leadership mechanics (overlap hours, async updates, onboarding plans, feedback across cultures) are on the Tech Lead site.
  4. The role description's "guide India-based developers" is a leadership question even when phrased technically; answer both halves.
  5. Don't present someone else's practice as your own experience; "here's what I'd do" is honest when you haven't done it.

Learn more: Design-first vs code-first, Device simulators and emulators, and the Tech Lead site's Working across US and India time zones.

An interviewer asks, "Our devices still have a Telnet API. Have you worked with Telnet?" You've studied it but never shipped one. What do you say?
Show answer

Be exact about your experience, then show understanding. "Start with the honest boundary, phrased truthfully for where you are: 'I haven't shipped a Telnet interface in production; I've studied the protocol and worked through runnable models of line-based command servers in Node', only if you have run and changed them. Then the substance: plain text over TCP with option negotiation, kept because room control systems and years of scripts speak it. Show you've read their docs: the command-line API on port 24 by default or 23, SSH on 22 as the encrypted option, both off by default, and Telnet with no port lockout, just a disconnect after three failed logins. Then the trade-off and the design you'd bring: off by default, trusted networks only, steer towards SSH, one command layer behind REST, SSH and Telnet, scripted session tests and fuzzing."

What’s happening
  1. If the second sentence isn't true yet, build a small line server yourself from the Telnet chapter first, so it is.
  2. Reading the site's model isn't the same as building one; until you have, say "I've studied the protocol and worked through a runnable model".
  3. Every technical sentence in the answer should be one you can defend under a follow-up.
  4. An answer like this is strong from someone who hasn't shipped Telnet, as long as every sentence in it is true.
  5. The same structure works for any gap: boundary, understanding, their context, the trade-off, what you'd build.

Learn more: Talking about this role in an interview and What Telnet is and why devices still expose it.

What questions would you ask them at the end?
Show answer

Questions that show you've read the public material and think about the problems they actually have. "Which Android baseline is the 4.x work on, given that the public 5.0 notes say 10 to 13 for the Gen2 bars? How do you decide which provider modes come to 5.x 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? How is the India team organised, how much overlap is there, and what do they own end to end? What do the lab and rollout pipeline look like for a 5.x build, and which health signals block promotion?"

What’s happening
  1. Each question comes from a public fact, so it signals research without claiming inside knowledge.
  2. Each one's answer tells you something you need to know about the job (how much is greenfield, how much is parity work, how much is leadership).
  3. One more from the public record: when a security change like 4.6.2's XSRF token breaks partner scripts, how is it communicated, and who decides on an escape hatch like 4.7.0's switch?
  4. Pick three or four for the time you have, and listen to the answers: follow-ups are better than a fifth question.
  5. The Tech Lead site has more for a lead role, including questions about how the team measures success.

Learn more: Talking about this role in an interview and the Tech Lead site's Questions to ask the interviewer.

What are the two traps in this interview, and how do you avoid them?
Show answer

Overclaiming Android platform depth, and presenting guesses about their product as facts. "An interviewer who works on the OS will ask one follow-up about Binder, SELinux policy or OTA partitions and find the edge in seconds, so I draw the boundary myself first: what I've built at the app level, what I understand at the design level, and that platform internals are the platform team's expertise. And if I say 'PolyOS uses X internally', I'm inventing; instead: 'the public docs describe the REST API and the command line; I don't know how they're built inside, but here's how I'd structure it', which gets credit for both the research and the design."

What’s happening
  1. The table of what they'll probe: web UI for hardware, REST API ownership, Telnet and command-line APIs, Android and the platform, the 4.x → 5.x migration, technical leadership, and judgement.
  2. Judgement is scored throughout: "the public docs don't say; here's how I'd find out" beats a confident guess.
  3. Label your design as your design whenever you describe how a Poly device might work inside.
  4. The same honesty applies to the role's internal details: don't name or describe internal projects from the job description as if you knew them.
  5. Practise by having someone ask "how do you know?" after every product claim you make.

Learn more: Talking about this role in an interview.

Written fresh for a device-platform full stack role. Runnable examples were run on Node 26; product facts come from public documentation, cited where used.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.