On a v16 POS installation, we spent an afternoon tracing a thermal printer failure through hw_escpos before realizing that module had not existed since v11. The answer was in hw_drivers. That kind of mismatch is avoidable once you know the version map, and the version map makes sense once you understand why the architecture was rebuilt twice.
The PosBox/IoT Box is not a product. It is an architectural pattern that was rebuilt under real constraints: maintenance at v12, security at v19. Each rebuild changed which module you need. The official docs treat each version as a fresh start; this article traces the line from v8 to v19.
We cover four architectural eras: v8 to v11 (PosBox with per-device driver modules), v12 (rebrand plus consolidation), v13 to v18 (stable IoT Box), and v19 onwards (ground-up rewrite). The article ends with a compact version map that tells you where to start debugging on any supported Odoo version.
Alternatives to the official IoT Box (direct USB, OCA iot_oca, third-party hardware integrations) are out of scope here; they are covered in a separate article.
Why a Hardware Proxy Exists at All
Before any version-specific detail, the constraint that made the PosBox necessary has to be clear. Everything else follows from it.
The browser security wall
Odoo POS is a single-page web application. Web applications run in a sandboxed browser environment that cannot access USB ports, serial ports, or any OS-level hardware interface. Every hardware interaction must go through HTTP.
WebUSB and WebSerial now exist in modern browsers, but they were not available in 2013 when the PosBox was introduced. Even today, they require explicit user permission per device and do not cover all device classes (serial scales, fiscal printers, and older USB HID devices are common gaps). The browser security wall is not a temporary limitation; it is a design property of the web platform.
Why local, not cloud
Routing every barcode scan and print job through the Odoo server would introduce unacceptable latency at the point of sale. A cashier scanning items cannot wait for a round trip to a remote server for each scan. The proxy must run on the local network, close to the hardware.
Running the proxy as a software service on the POS workstation (a Windows or Mac retail PC) creates its own problems: antivirus conflicts, inconsistent USB enumeration across operating systems, and a dependency on the operator not modifying the machine. A dedicated device, specifically a Raspberry Pi, avoids all of these. The Pi runs a controlled Linux environment, has predictable USB behavior, and is cheap enough to dedicate to the role.
The constraint that shaped everything
The single constraint that produced the PosBox architecture: hardware access requires OS-level privileges; browsers do not have them. A local privileged process must bridge the gap.
Every subsequent rewrite is either expanding scope or improving maintainability within that same constraint. The constraint itself does not change across any Odoo version. This is worth stating plainly because it explains why the architecture looks similar at a high level even when the module names change completely.
Era 1: PosBox v8 to v11
In the original PosBox architecture, the hardware proxy is split across two layers: a core controller module (hw_proxy) and a set of per-device driver modules (hw_escpos, hw_scanner, hw_scale, and others).
hw_proxy: the controller loop
hw_proxy is the HTTP and RPC proxy core [2]. It exposes status endpoints and RPC call handlers that the browser-based POS communicates with. The controller loop does two things: it polls USB ports for HID class-7 devices via PyUSB, and it scans serial ports for RS-232 devices [3].
When a device is detected, the loop looks for a matching driver in a shared drivers dictionary. Each driver module registers itself into this dictionary at import time and implements a get_status() method. The proxy calls get_status() to surface hardware state to the POS UI. This registry pattern is simple and explicit: the proxy does not need to know anything about the hardware; the driver does.
The per-device driver modules
Each hardware class has its own module [1]:
hw_escposhandles ESC/POS thermal printers and cash drawers. Transport: USB HID and RS-232. Python dependencies:usb.core,serial,qrcode[4].hw_scannerhandles barcode scanners. Transport: USB HID viaevdev, treating the scanner as keyboard input [5].hw_scalehandles weighing scales, including Mettler Toledo Ariva. Transport: RS-232 serial with a polling loop that reads weight frames [6].hw_screenhandles customer-facing display screens.hw_blackbox_behandles Belgian fiscal printers and is subject to regulatory compliance requirements with its own release cadence.hw_posbox_upgradehandles remote software updates for the Raspberry Pi image [7].
The transport matters more than the module name. hw_escpos uses USB HID and sends ESC/POS byte commands; hw_scale opens a serial port and reads fixed-width frames. These are different failure modes and different debugging paths.
Why separate modules made sense then
Each device class had different Python dependencies. PyUSB, evdev, and pyserial do not need to coexist in the same import path if the modules are separate. A broken scale driver should not kill the printer. hw_blackbox_be tracks Belgian fiscal regulations, not Odoo POS releases, so keeping it isolated meant it could be updated independently.
Per-module isolation was the right call at small scale. It became a maintenance burden as the number of supported devices grew, because every new device required a new module with its own manifest, dependencies, and release tracking.
The PosBox OS
The device runs a dedicated PosBox OS image on Raspberry Pi [1]. The image ships with all Python dependencies for the driver modules pre-installed. Remote image updates are handled by hw_posbox_upgrade, which allows updating the Pi software without physical access to the device [7].
Era 2: v12, the Rename Was a Footnote, the Consolidation Was Not
Two things happened at v12: the device was renamed from PosBox to IoT Box, and the per-device driver modules were consolidated into a single hw_drivers module. The rename gets remembered; the consolidation is the part that matters for developers.
The two-line rebrand
In hw_posbox_homepage/__manifest__.py, the module name changed from 'PosBox Homepage' to 'IoT Box Homepage' [9]. The website URL shifted from /page/point-of-sale to /page/point-of-sale-hardware. This signals a broader positioning beyond retail POS, but it is a manifest change. The underlying architecture is untouched at the point of the rename.
hw_drivers: the real v12 change
The individual hw_* driver modules were removed from the main Odoo repository and consolidated into a single hw_drivers module [10]. One module, one release cycle, one dependency manifest. The maintenance burden of per-module versioning is eliminated.
The driver registry pattern persists inside hw_drivers. The shared drivers dictionary and get_status() interface remain; they are just packaged differently. If you had hw_escpos in v11, you now look in hw_drivers. The architecture is the same; the packaging changed.
Backward compatibility: hw_proxy stays
hw_proxy remains as a dependency of hw_drivers through v18 [10][11]. The HTTP proxy layer is unchanged. The proxy still exposes the same endpoints to the browser. This is a packaging change, not a protocol change. The transition from v11 to v12 did not break existing POS configurations.
What “IoT” signalled
The URL change to /page/point-of-sale-hardware and the IoT Box name signal intent. The device is now positioned for use cases beyond POS: payment terminals, weighing stations, and in later versions industrial I/O. The architecture in v12 does not yet reflect this fully. hw_drivers is still POS-hardware-centric. But the scope declaration at v12 seeds the v19 rewrite by establishing that the device is meant for general hardware integration, not just retail POS.
Era 3: v13 to v18, the Stable IoT Box
The defining feature of this era is what Odoo did not change. hw_drivers carries through v13 to v18 without architectural modification.
The long plateau
If you open addons/hw_drivers/__manifest__.py on any v13 to v18 source tree, it looks structurally identical to v12 [11]. Six major Odoo versions on the same module architecture. This is the era most active Odoo deployments are on today: v14, v16, and v17 are all common production versions.
Scope expands, architecture does not
The IoT Box is marketed through this era for non-POS hardware: payment terminals, weighing stations, industrial I/O sensors. New device types are added to hw_drivers over time. No new abstraction layer is introduced. No new protocol. Each new device type is a new driver registered into the same drivers dictionary inside the same hw_drivers module. The pattern from v8 is still running.
The deliberate non-rewrite
A six-version plateau on a single architecture is a decision, not neglect. The hw_proxy HTTP layer worked well enough for the retail use case. The overhead of a rewrite was not justified while the architecture was meeting requirements.
The constraint that eventually forced a rewrite was security. Certificate-based authentication for IoT devices requires a different communication model from the unauthenticated HTTP proxy that hw_proxy provides. Adding certificate auth to hw_proxy was architecturally invasive enough that a clean rewrite was preferable.
Practical implications for v13 to v18 developers
Start at hw_drivers for any hardware debugging on these versions. hw_proxy is present as a dependency but is not where driver logic lives. The module boundary is clean: hw_drivers contains all driver code; hw_proxy is the HTTP plumbing underneath it.
Do not look for hw_escpos or hw_scanner on these versions. They do not exist as standalone modules. The code for ESC/POS printers and barcode scanners is inside hw_drivers.
Era 4: v19, Ground-Up Rewrite
v19 is a clean-break rewrite. The module names change, the communication model changes, and the security story changes. This is not an incremental upgrade from v18.
What forced a rewrite
The proximate cause is security. SSH certificate authority support, specifically keypair generation and certificate-based authentication for IoT devices, required a different communication model from the hw_proxy HTTP layer [15]. The old model had no authentication story. Adding it was architecturally invasive: the HTTP endpoints in hw_proxy were designed for unauthenticated local-network use. Retrofitting certificate auth onto that layer would have produced something worse than a clean rewrite.
The secondary driver is the scope expansion that was declared at v12 but never fully implemented. The v19 rewrite is the point where the architecture catches up to the “IoT beyond POS” positioning.
iot_drivers: the new driver layer
iot_drivers replaces hw_drivers [12]. The conceptual role is the same: hardware drivers for connected devices. The implementation is rebuilt from scratch to support the new authentication model.
hw_posbox_homepage persists in v19 as the web UI entrypoint for the device, but it now serves a dedicated Odoo IoT frontend rather than a web-interface overlay on top of the proxy [12]. The user-visible interface is the same; the backend it connects to is different.
iot_base: network utilities and the JavaScript device controller
iot_base is a new module with no direct v8-v18 equivalent [13]. It adds network utilities used by the IoT Box OS and the Odoo server communication layer. More architecturally significant: it ships a JavaScript device-controller library for managing device interactions in the browser.
This is a meaningful shift. In all prior eras, device interaction logic lived entirely in the server-side proxy. With iot_base, some of that logic moves client-side. The browser can now participate in device management rather than being a pure consumer of proxy endpoints.
iot_box_image: automated image builds
iot_box_image automates building a bootable Raspberry Pi OS image [14][15]. This exists in earlier versions as an ad-hoc process; v19 makes it a versioned, automated build. The image uses YY.MM versioning (year and month), so releases are time-versioned rather than tied to Odoo major versions [14]. This lets the IoT Box OS ship security updates and driver updates independently of the Odoo release cycle.
The build script (build_image.sh) handles image construction [15]. Post-init scripts (overwrite_before_init, overwrite_after_init) configure OS-level settings. The full internals of these scripts are not covered in this article; the architectural point is that image builds are now first-class, automated, and independently versioned.
SSH certificate authority: the security story
IoT devices in v19 are authenticated before communication begins. Keypair generation and certificate-based auth are handled at the OS and infrastructure level [15]. This replaces the unauthenticated HTTP model that hw_proxy used from v8 to v18. For enterprise deployments and multi-site installations, this is a meaningful security improvement. For single-store retail deployments, it is transparent but provides a more defensible posture.
Version Map: Which Driver Layer Are You On?
Three eras, three driver layers. The map is compact.
v8 to v11:
- Controller:
hw_proxy - Drivers: individual
hw_*modules (hw_escpos,hw_scanner,hw_scale,hw_screen,hw_blackbox_be,hw_posbox_upgrade) - Start debugging at the specific driver module for the hardware type involved
v12 to v18:
- Controller:
hw_proxy(dependency, plumbing only) - Drivers:
hw_drivers(consolidated single module) - Start debugging at
hw_driversfor all hardware issues [10][11]
v19 and later:
- Drivers:
iot_drivers(replaceshw_drivers) [12] - Network and client-side:
iot_base[13] - OS image:
iot_box_image(time-versioned, automated builds) [14] - Start debugging at
iot_driversfor hardware issues; checkiot_basefor network and browser-side device controller issues
One practical rule
For any hardware debugging session, start at the driver layer for the Odoo version installed. Do not look in hw_proxy for driver logic on v12 and later. Do not look for hw_escpos on v12 and later. Do not look in hw_drivers on v19. The module names are not backward-compatible across eras.
What persists across all eras
Three things do not change across any Odoo version:
- The fundamental constraint: the browser cannot access hardware directly; a local privileged process must proxy. This is as true in v19 as it was in v8.
- The Raspberry Pi as the target device. The Pi is not mandatory in all eras (any Linux device can run the software), but the official OS images are Pi-targeted throughout.
hw_posbox_homepageas the device web UI entrypoint. The name persists from v8 to v19, even as the backend it connects to changes completely [1][9][12].
Conclusion
The PosBox and IoT Box story is three architectural decisions driven by three different pressures. The original v8 design solved the browser-hardware gap with a simple HTTP proxy and isolated driver modules. The v12 consolidation into hw_drivers solved a maintenance problem. The v19 rewrite solved a security problem.
The rename from PosBox to IoT Box is a two-line manifest change. The rewrites are the story.
For any Odoo installation, the version map above tells you where to look. One module name, looked up once, saves an afternoon.
Sources
All files are in the odoo/odoo open-source repository under their respective version branch (e.g. 10.0, 12.0, 17.0).
Era 1 — v8 to v11 (PosBox):
[1] addons/hw_posbox_homepage/__openerp__.py (v8): module manifest and description
[2] addons/hw_proxy/__manifest__.py (v10): module dependencies and description
[3] addons/hw_proxy/controllers/main.py (v10): controller loop, USB and serial scanning
[4] addons/hw_escpos/__manifest__.py (v10): ESC/POS driver dependencies
[5] addons/hw_scanner/__manifest__.py (v10): barcode scanner driver via evdev
[6] addons/hw_scale/__manifest__.py (v10): weighing scale driver via RS-232
[7] addons/hw_posbox_upgrade/__manifest__.py (v10): remote update module
[8] addons/hw_posbox_homepage/__manifest__.py (v11): pre-rename manifest
Era 2 — v12 (IoT Box rename + consolidation):
[9] addons/hw_posbox_homepage/__manifest__.py (v12): rename to IoT Box Homepage, URL change
[10] addons/hw_drivers/__manifest__.py (v12): consolidated driver module
Era 3 — v13 to v18 (Stable IoT Box):
[11] addons/hw_drivers/__manifest__.py (v17): unchanged structure through stable era
Era 4 — v19 (Ground-up rewrite):
[12] addons/iot_drivers/__manifest__.py (v19): new driver module replacing hw_drivers
[13] addons/iot_base/__manifest__.py (v19): network utilities and JavaScript device controller
[14] addons/iot_box_image/__manifest__.py (v19): automated Raspberry Pi image build module
[15] addons/iot_box_image/build_image.sh (v19): image build script and post-init configuration