# Odoo PosBox and IoT Box: History, Architecture, and Version Map

> The PosBox became the IoT Box, and the driver layer was rebuilt twice: once for maintenance in v12, once for security in v19. A version map telling you where to start debugging.

**Date:** 2026-04-25
**Source:** <https://trobz.com/insights/posbox-iotbox/>

---



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_escpos` handles ESC/POS thermal printers and cash drawers. Transport: USB HID and RS-232. Python dependencies: `usb.core`, `serial`, `qrcode` [4].
- `hw_scanner` handles barcode scanners. Transport: USB HID via `evdev`, treating the scanner as keyboard input [5].
- `hw_scale` handles weighing scales, including Mettler Toledo Ariva. Transport: RS-232 serial with a polling loop that reads weight frames [6].
- `hw_screen` handles customer-facing display screens.
- `hw_blackbox_be` handles Belgian fiscal printers and is subject to regulatory compliance requirements with its own release cadence.
- `hw_posbox_upgrade` handles 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_drivers` for all hardware issues [10][11]

**v19 and later:**
- Drivers: `iot_drivers` (replaces `hw_drivers`) [12]
- Network and client-side: `iot_base` [13]
- OS image: `iot_box_image` (time-versioned, automated builds) [14]
- Start debugging at `iot_drivers` for hardware issues; check `iot_base` for 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_homepage` as 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`](https://github.com/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

