Bin setups
A layout (bin setup) describes your put walls: each wall, where it stands, and every bin with its size and limits. You
create it once with POST /api/layouts and replace it with PUT /api/layouts/{layoutId} when the walls change. Waves
always refer to a layout.
You can write the layout JSON yourself, or let the install draft it from a CAD drawing, a spreadsheet, a grid description
or a photo. Every import returns a draft with warnings and saves nothing: check it (the setup editor on the admin page,
or the <slotting-setup> component, shows it), fix what's wrong, then save it.
The layout model#
{
"id": "PUTWALL-A",
"name": "Put wall A, dock 3",
"unit": "cm",
"weightUnit": "kg",
"fillFactor": 0.85,
"walls": [
{
"id": "A",
"kind": "fixed",
"position": { "x": 0, "y": 0 },
"rotation": 0,
"bins": [
{ "id": "A-01-01", "x": 0, "y": 30, "width": 40, "height": 30, "depth": 40, "maxWeight": 15 },
{ "id": "A-01-02", "x": 40, "y": 30, "width": 40, "height": 30, "depth": 40, "maxWeight": 15, "maxUnits": 20 },
{ "id": "A-02-01", "x": 0, "y": 0, "width": 40, "height": 30, "depth": 40, "disabled": true }
]
}
]
}Units. Every length of a layout is in its unit (mm, cm, m, in or ft) and every weight in its weightUnit
(kg, g, lb or oz). The item dimensions and weights you send with a wave must use the same units: the service never
converts.
Bins on the wall face. Look at the wall from the side where the operator puts. x is the distance of a bin's
bottom-left corner from the wall's left edge, y its height above the floor (or above the wall's lowest bin; only the
differences matter). width and height are the opening, depth how far the bin goes into the wall:
y ▲
│ ┌────────┬────────┬────────┐
│ │A-01-01 │A-01-02 │A-01-03 │ row 1 (top)
30 ┤ ├────────┼────────┼────────┤
│ │A-02-01 │A-02-02 │A-02-03 │ row 2
0 ┼──┴────────┴────────┴────────┴──▶ x
0 40 80 120| Field | ||
|---|---|---|
id (layout) |
required | 1 to 100 of A-Z a-z 0-9 . _ : -, starting with a letter or digit |
unit, weightUnit |
required | See above |
fillFactor |
0.85 | The share of a bin's volume that can really be filled (0.1 to 1). A wave can override it. See Capacity |
walls[].id |
required | Unique in the layout |
walls[].kind |
required | fixed, or mobile for a put cart or a wall on wheels that can move while waves run |
walls[].position |
Where the wall's bottom-left corner stands on the floor (x, y in layout units). Needed only when an order may get bins on two walls (see below) |
|
walls[].rotation |
0 | The direction the wall face runs along the floor, in degrees counter-clockwise from the floor's x axis |
bins[].id |
required | The location code printed on the bin label. Unique in the whole layout, not just the wall: scans return it |
bins[].x, y |
required | Bottom-left of the opening on the wall face |
bins[].width, height, depth |
required | The opening and the depth, 0.01 to 1,000,000 |
bins[].maxWeight |
no limit | The most the bin may hold, in weightUnit |
bins[].maxUnits |
no limit | The most units the bin may hold. Set it for bins that take items without dimensions (why) |
bins[].disabled |
false |
Out of service: never assigned |
bins[].lightAddress |
the bin id | The address of the bin's put-to-light module: 1 to 64 of A-Z a-z 0-9 . _ : / -, unique in the layout, and not another bin's id |
The install checks a layout when it is saved and answers 422 invalid_layout with the problems listed in issues:
- wall ids unique, bin ids unique across the layout; ids of walls, bins, orders and SKUs are 1 to 100 characters, without
control characters or
#; - bins of one wall must not overlap (the message names both bins);
- at most 200 walls and 10,000 bins per layout; coordinates within ±10,000,000.
Where the walls stand#
Bins of one wall are measured on the wall itself, so a layout with one wall needs no positions. When an order may get bins
on two walls (a big order on a station with two walls, or a wall pair facing each other), slotting needs to know how far
apart they are: give each wall a position and rotation on the floor plan. Without positions, bins of different walls
count as very far apart, so an order only spans two walls when there is no other way.
Mobile walls (put carts) can be moved while waves run: PATCH /api/layouts/{layoutId}/walls/{wallId} with
{ "position": { "x": 1200, "y": 300 }, "rotation": 90 }. It changes only the choice of bins from then on; units already put
stay where they are. A wave can also be limited to some walls with wallIds (for example the carts docked at a station).
Changing a layout#
PUT /api/layouts/{layoutId} replaces the layout. Adding walls and bins is always allowed. Removing, shrinking or
disabling a bin that an active wave holds is refused with 409 layout_in_use: release the orders or close the wave first.
Moving a held bin to another wall, or adding or lowering the maxWeight / maxUnits of a held bin, is refused the
same way. DELETE /api/layouts/{layoutId} is refused while the layout has a wave that is not closed.
From a CAD drawing (DXF)#
POST /api/import/dxf with { "content": "<the DXF file as text>", "options": { ... } } reads a front elevation of one
or more walls and returns a draft layout.
What the drawing must look like#
- ASCII DXF, versions R12 to 2018. DWG files and binary DXF are refused with a message: export ASCII DXF (see below).
- A front view of the wall, as the operator sees it. Plan views, 3D models and isometric views don't work: the importer reads rectangles in the drawing's X/Y plane.
- Bins as rectangles, in either form (they can be mixed):
- closed rectangles: LWPOLYLINE or POLYLINE with 4 corners and straight, horizontal and vertical sides;
- a grid of horizontal and vertical LINEs (shelf and divider lines): every cell they enclose becomes a bin, so a rectangle drawn as 4 separate lines works too. A rectangle around other bins (the wall's outline) is recognised as the outline and is not a bin.
- Bin ids from text. A TEXT, MTEXT or block attribute inside a bin becomes its id (
A-01-01). Bins without text get an id frombinIdPattern. A label that appears twice is used once; the other bin gets a generated id and a warning. - Blocks are expanded (moved, scaled, rotated in steps of 90 degrees; other rotations are skipped with a warning), so a bin drawn once as a block and inserted many times works.
- Units: the drawing's units setting (
$INSUNITS: millimetres, centimetres, metres, inches or feet) is used. If the drawing has none, passunit. Lengths are never converted:unitsays what the drawing's numbers mean. - Several walls: put each wall's geometry on its own layer named
WALL-<id>(WALL-A,WALL-B). TheWALL-prefix is removed from the wall id. Other layers are then ignored. - Clean up first: hide or delete dimensions, hatches, title blocks and furniture, or pass
layerto read only the layers with the bins. Double shelf lines (a shelf drawn as two lines 2 cm apart) make thin cells:minBinSizedrops them.
Limits: 20 MB of text, 1,000,000 entities (counting expanded blocks), 10,000 bins.
| Option | Default | |
|---|---|---|
depth |
required | The depth of every bin: an elevation has no depth. Edit single bins in the draft afterwards |
unit |
the drawing's $INSUNITS |
mm, cm, m, in, ft. Required when the drawing has no units setting |
layer |
every layer | A layer name or a list: only geometry on these layers (case-insensitive). Text is read from every layer |
splitWalls |
layer if any layer is named WALL-*, else none |
layer: one wall per layer; none: everything is one wall |
wallId |
W1 |
The wall id when the drawing is one wall |
binIdPattern |
{wall}-{row}-{col} |
The id of a bin without text. {row} counts from the top and {col} from the left, from 1, at least two digits; {n} numbers the bins in reading order |
minBinSize |
0 | Ignore rectangles narrower or lower than this (drawing units) |
layoutId, name |
imported |
The draft's id and name |
weightUnit |
kg |
# Read the file into the JSON body with jq, then import:
jq -Rs '{content: ., options: {depth: 40, unit: "cm", binIdPattern: "{wall}-{row}-{col}"}}' wall-a.dxf \
| curl -s -X POST http://127.0.0.1:10083/api/import/dxf -H 'Content-Type: application/json' -d @-The answer is { "layout": { ... }, "warnings": [ ... ] }. Bin positions are measured from the bottom-left of each wall's
bins, wherever the wall sits in the drawing.
Exporting ASCII DXF#
- AutoCAD (and AutoCAD LT): open the elevation, then Save As, file type AutoCAD 2018 DXF (or any older DXF
version). In the Save As dialog choose Tools › Options › DXF Options, set the format to ASCII, and save. The
DXFOUTcommand opens the same dialog. If the elevation is a layout (paper space) view of a 3D model, export the 2D elevation itself instead: use FLATSHOT (or the section/elevation tools) to make 2D geometry, then save that as DXF. - BricsCAD, DraftSight, ZWCAD, progeCAD: Save As › DXF (choose ASCII, not binary, where the dialog offers it).
- Revit: open the elevation view of the wall, File › Export › CAD Formats › DXF. Revit exports ASCII DXF.
- SketchUp Pro: set Camera › Parallel Projection and the front view, then File › Export › 2D Graphic › DXF.
- FreeCAD, LibreCAD, QCAD: save or export as DXF (these write ASCII DXF).
- Only a PDF or a paper drawing? Use the CSV or grid import instead, or the photo import for a first draft.
To check a file: open it in a text editor. An ASCII DXF starts with lines like 0, SECTION, 2, HEADER. A binary DXF
starts with AutoCAD Binary DXF, and a DWG with AC10.
From a spreadsheet (CSV)#
POST /api/import/csv with { "content": "<the CSV text>", "options": { "unit": "cm", ... } }. One row per bin, with a
header row. Column names are matched without regard to case, spaces, _ and -, and common aliases work:
| Column | Aliases | |
|---|---|---|
bin |
binId, location, locationCode, locationId, slot, cubby, id, code |
Required. The bin id |
wall |
wallId, putWall, wallName |
The wall; default the wallId option, else W1 |
x, y |
left, bottom |
Bottom-left on the wall face. Either x and y, or row and col |
row, col |
level, shelf / column |
Instead of x / y: row 1 is the top. Positions are computed by stacking: each column is as wide as its widest bin and each row as tall as its tallest |
width, height, depth |
w, h, d |
Required per row, unless given once as options |
maxWeight |
weightLimit, maxWt |
|
maxUnits |
unitLimit, capacity, maxQty |
|
disabled |
inactive |
yes, true, 1, x = out of service; empty, no, false, 0 = in service |
wallKind |
kind, wallType |
fixed or mobile |
wallX, wallY, wallRotation |
rotation |
The wall's floor position and rotation (once per wall is enough) |
lightAddress |
light, light address, address |
The bin's put-to-light address; empty = the bin id. Checked like bins[].lightAddress above (an invalid or repeated address is invalid_layout) |
- The delimiter is detected from the header:
,,;or tab. Quoted fields ("A,1") work, and so do decimal commas (40,5) in files that use;. A byte-order mark (Excel's "CSV UTF-8") is ignored. Unknown columns are ignored with a warning. - Bad rows are reported together (
invalid_import, with the line numbers indetails.errors), so you can fix the file in one go. Limits: 20 MB, 10,000 bins.
| Option | Default | |
|---|---|---|
unit |
required | The unit of the lengths in the file |
width, height, depth |
Used for rows that leave those columns empty | |
wallId |
W1 |
For rows without a wall |
layoutId, name, weightUnit |
imported, -, kg |
The simple form, with rows and columns:
wall;location;row;col;width;height;depth;maxUnits
A;A-01-01;1;1;40;30;40;20
A;A-01-02;1;2;40;30;40;20
A;A-02-01;2;1;40;30;40;
A;A-02-02;2;2;40;30;40;A regular grid#
POST /api/import/grid generates one wall of equal (or row- and column-wise sized) bins. The body is the grid itself:
{ "unit": "cm", "wallId": "B", "rows": 4, "cols": 6, "cellWidth": 30, "cellHeight": 25, "depth": 45, "gap": 2, "maxUnits": 15 }| Field | Default | |
|---|---|---|
unit, wallId, depth |
required | |
rows |
required | A count (then cellHeight is required) or the row heights, top to bottom: [25, 25, 40] |
cols |
required | A count (then cellWidth is required) or the column widths, left to right |
cellWidth, cellHeight |
The bin size when rows / cols are counts |
|
gap |
0 | Space between neighbouring bins (shelf and divider thickness) |
origin |
{ "x": 0, "y": 0 } |
Bottom-left of the grid on the wall face |
binIdPattern |
{wall}-{row}-{col} |
As for DXF |
kind |
fixed |
mobile for a put cart |
maxWeight, maxUnits |
For every bin | |
layoutId, name, weightUnit |
imported, -, kg |
To build a layout of several walls, import each wall and combine the drafts' walls into one layout (the setup editor does
this when you import several times).
From a photo (AI)#
POST /api/import/photo drafts one wall from a photo. An AI vision model finds the wall and the bin openings and reads
the bin labels it can see; the install turns that into real sizes with the measurements you give.
{ "image": "<base64 of the JPEG, PNG or WebP>", "mediaType": "image/jpeg",
"hints": { "unit": "cm", "wallWidth": 240, "wallHeight": 180, "depth": 45, "rows": 6, "cols": 8, "wallId": "C" } }| Hint | Default | |
|---|---|---|
unit |
required | The unit of the measurements |
depth |
required | The depth of every bin (a front photo doesn't show it) |
wallWidth, wallHeight |
The measured width and height of the bin area. Give at least one; the other is then estimated from the photo's proportions (with a warning). Without either, the model looks for something of known size in the photo (a tape measure, a pallet) and the result is a rough guess | |
rows, cols |
The grid you expect: passed to the model, and checked (a warning when the result differs) | |
wallId |
W1 |
|
binIdPattern |
{wall}-{row}-{col} |
Ids of bins whose label can't be read |
layoutId, name, weightUnit |
imported, -, kg |
Taking the photo: stand straight in front of the middle of the wall, far enough back to get the whole wall in one picture, with even light and the bin labels readable. Perspective and lens distortion are not corrected, so a photo taken from the side gives skewed sizes (there is a warning when the measured proportions and the photo differ by more than 15%). The draft always says that its sizes come from a photo: measure a few bins and correct the draft before saving it.
Phone photos can be taken in portrait or landscape: the rotation the phone stores in the photo (EXIF orientation) is honoured, so the draft matches the photo as you see it (the warnings say when the photo was rotated).
Limits: the image is JPEG, PNG or WebP (checked by its content, not its name), up to 15 MB; up to 2,000 bins per photo.
iPhones save HEIC by default: export or share the photo as JPEG (or set Camera > Formats > Most Compatible).
The import takes as long as the model needs, typically 10 to 60 seconds (SLOTTING_AI_TIMEOUT_MS, default 60 s).
Choosing an AI provider#
Photo import is off until the operator configures a provider with environment variables (DXF, CSV, grid and JSON work
without one). GET /v1/info (ai) and the Status page show whether it is configured; without it the import
answers 503 ai_not_configured, and a provider error or timeout answers 502 ai_failed.
| Provider | SLOTTING_AI_PROVIDER |
Settings |
|---|---|---|
| Anthropic (Claude) | anthropic |
SLOTTING_AI_API_KEY or ANTHROPIC_API_KEY. SLOTTING_AI_MODEL optional (default claude-opus-5-5) |
| Amazon Bedrock | bedrock |
SLOTTING_AI_MODEL: a model or inference profile id with image input. AWS_REGION, and AWS credentials from the default chain (environment, shared config, instance or task role). No API key |
| OpenAI | openai |
SLOTTING_AI_API_KEY or OPENAI_API_KEY, and SLOTTING_AI_MODEL (a vision-capable model) |
| Azure OpenAI | azure-openai |
AZURE_OPENAI_ENDPOINT (https://<resource>.openai.azure.com), AZURE_OPENAI_DEPLOYMENT (or SLOTTING_AI_MODEL), SLOTTING_AI_API_KEY or AZURE_OPENAI_API_KEY. AZURE_OPENAI_API_VERSION optional (default 2024-10-21) |
| Google Gemini | google |
SLOTTING_AI_API_KEY or GEMINI_API_KEY, and SLOTTING_AI_MODEL (a Gemini model with image input) |
| OpenAI-compatible server (Ollama, vLLM, LM Studio, LiteLLM, ...) | openai-compatible |
SLOTTING_AI_BASE_URL (for example http://ollama.internal:11434/v1) and SLOTTING_AI_MODEL (a vision model it serves). SLOTTING_AI_API_KEY only if the server needs one |
SLOTTING_AI_BASE_URL also overrides the endpoint of the other providers, for example to go through your proxy or gateway.
The full list of settings is in Install and operate.
Privacy. The photo is sent to the configured provider, together with the instructions for the model; nothing else (no orders, no layouts). With a cloud provider (Anthropic, OpenAI, Gemini, Azure OpenAI, Bedrock) the photo leaves your network and is processed under that provider's terms and data-retention rules, so check what may be visible in it (people, screens, documents) before uploading. To keep photos inside your network, use
openai-compatiblewith a model you host yourself. The install never logs the image or the API key.