Skip to content

vbwanere/manufacturingOS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Manufacturing OS — Design Document & Learning Journal

What This Is

This is a C++ library for managing a machine shop floor. It is designed as a Manufacturing Operating System — not a simple optimization tool, but a full platform that models cutting tools, machine assets, products, routings, and cost optimization.

It is also a deliberate C++ learning project. Every design decision and every file was chosen to teach a specific concept. This document explains both — why we built it this way, and what C++ concept each decision demonstrates.


Why C++

The core of this system is compute-heavy:

  • Cost per part calculations across hundreds of machines and thousands of tool combinations
  • Break-even analysis for tool economics
  • Eligibility filtering and optimization across large datasets
  • Future: real-time machine monitoring

C++ gives us performance, direct memory control, and a strongly typed system that catches errors at compile time rather than runtime. For a system that will eventually run on a shop floor where wrong answers cost money, this matters.


1. Mental Model: Three Layers

┌─────────────────────────────────┐
│         Interface Layer         │  CLI now, GUI in future
├─────────────────────────────────┤
│         Service Layer           │  Cost engine, optimization, tool economics
├─────────────────────────────────┤
│         Data Layer              │  Entities, persistence, repositories
└─────────────────────────────────┘

Each layer only talks to the layer below it. The GUI never touches the database directly. The optimization engine never reads a CSV directly. Clean separation means each layer can be replaced independently.


2. Repository Structure

manufacturing-os/
│
├── CMakeLists.txt                  ← build system
├── README.md
├── LICENSE
│
├── include/                        ← public API headers (what users of library see)
│   └── mfgos/
│       ├── core/
│       │   ├── Types.h             ← common typedefs, enums, units
│       │   └── Result.h            ← Result<T, Error> return type — no raw exceptions
│       │
│       ├── tool/
│       │   ├── IToolComponent.h    ← abstract interface
│       │   ├── ToolAssembly.h
│       │   └── ToolInventory.h
│       │
│       ├── machine/
│       │   ├── IMachineAsset.h     ← abstract interface
│       │   └── ProcessProfile.h
│       │
│       ├── product/
│       │   ├── Product.h
│       │   ├── Operation.h
│       │   ├── Routing.h
│       │   └── ToolingPackage.h
│       │
│       ├── repository/
│       │   ├── IToolRepository.h   ← abstract interfaces
│       │   ├── IMachineRepository.h
│       │   └── IProductRepository.h
│       │
│       └── engine/
│           ├── ICostEngine.h
│           ├── IOptimizationEngine.h
│           └── IToolEconomicsEngine.h
│
├── src/                            ← implementations (private)
│   ├── tool/
│   │   ├── ToolHolder.cpp
│   │   ├── ColletChuck.cpp
│   │   ├── CutterBody.cpp
│   │   ├── SolidCuttingTool.cpp
│   │   ├── Insert.cpp
│   │   ├── Adapter.cpp
│   │   └── ToolAssembly.cpp
│   │
│   ├── machine/
│   │   ├── MillingMachine.cpp
│   │   ├── TurningMachine.cpp
│   │   └── MillTurnMachine.cpp
│   │
│   ├── product/
│   │   ├── Product.cpp
│   │   ├── Operation.cpp
│   │   └── ToolingPackage.cpp
│   │
│   ├── repository/
│   │   ├── csv/
│   │   │   ├── CSVToolRepository.cpp
│   │   │   ├── CSVMachineRepository.cpp
│   │   │   └── CSVProductRepository.cpp
│   │   └── json/
│   │       └── JSONProductRepository.cpp
│   │
│   └── engine/
│       ├── CostEngine.cpp
│       ├── OptimizationEngine.cpp
│       └── ToolEconomicsEngine.cpp
│
├── data/                           ← sample data files
│   ├── cutting_tools/
│   │   ├── tool_library.csv
│   │   └── tool_inventory.csv
│   ├── machines/
│   │   └── machines.csv
│   └── products/
│       └── products.json
│
├── tests/                          ← unit tests
│   ├── tool/
│   │   └── test_tool_assembly.cpp
│   ├── machine/
│   │   └── test_machine_eligibility.cpp
│   ├── engine/
│   │   └── test_cost_engine.cpp
│   └── economics/
│       └── test_tool_economics.cpp
│
├── examples/                       ← how to use the library
│   ├── basic_cost_query.cpp
│   ├── tool_economics_demo.cpp
│   └── optimization_demo.cpp
│
└── docs/
    ├── architecture.md
    ├── data_formats.md
    └── api_reference.md

Why include/ and src/ are separated

include/mfgos/ is the contract — what we promise to anyone who uses this library. These headers are stable. Change them and every user must update their code.

src/ is the implementation — how we keep that promise. Rewrite any .cpp completely and nobody using the library notices. They only depend on the headers.

This is not about secrecy. The full source is on GitHub. It is about interface stability. The same reason you use std::vector without caring how push_back() is implemented internally.

Why include/mfgos/ has a namespace folder

If another library also has a Types.hpp, there is no collision:

#include "mfgos/core/Types.hpp"    // this library
#include "somelib/core/Types.hpp"  // another library — no conflict

Naming Conventions

Consistent naming is not style preference — it is communication. Every name tells you something:

Thing Convention Example
Types, Classes, Enums PascalCase MachineState, ToolHolder
Variables, Methods camelCase m_assetID, getLife()
Constants UPPER_SNAKE_CASE MAX_TOOL_LIFE
Interfaces I prefix + PascalCase IToolComponent
Member variables m_ prefix + camelCase m_totalCycles
Enum values PascalCase inside enum class MachineState::Running
Files PascalCase ToolHolder.hpp

The m_ prefix is critical. Inside a method body:

Result<Minutes> getLife() const {
    Minutes remaining = m_totalCycles - m_usedCycles;  // m_ = member, no m_ = local
}

You instantly know what is an object's data vs a local variable.


3. Core Design Principles

1. Abstraction

Hide implementation details behind clean interfaces. The caller of getLife() never needs to know whether life is calculated from cycles, wear state, or time. They just call getLife() and get Result<Minutes> back.

2. Encapsulation

Private data cannot be touched directly. The only way to create a valid Result is through Result::ok() or Result::fail(). The only way to get data out is through value() and error() which have safety checks. Prevents corrupted state.

3. Inheritance — "is-a"

IToolComponent
└── ToolHolder      ← ToolHolder IS-A IToolComponent
└── Insert          ← Insert IS-A IToolComponent

Shared data (m_cost, m_manufacturer, m_quantity) defined once in the base. Every subclass gets it automatically.

4. Composition — "has-a"

ToolAssembly
└── ToolComponent[]   ← Assembly HAS-A list of components

An assembly owns components. It does not inherit from them.

5. Polymorphism — treat different things uniformly

std::vector<IToolComponent*> stack = {holder, collet, insert};
for (auto component : stack) {
    component->getLife();  // each one answers differently, caller doesn't care
}

6. Dependency Inversion

OptimizationEngine depends on IMachineRepository — the interface — not on CSVMachineRepository — the concrete implementation. Swap CSV for a database later and the engine never changes.

7. Open/Closed

Add a new machine type by adding a new subclass. Never modify existing code. Existing behavior cannot break.


4. Domain Model

4.1 Tool Domain

A cutting tool in the shop is never a single object — it is a ToolAssembly: an ordered stack of components.

ToolAssembly
├── ToolHolder        (machine interface — CAT40, BT40, HSK63, Capto)
├── ColletChuck       (clamping — ER32, hydraulic, shrink fit)
├── CutterBody        (insert-based bodies)
│   └── Insert[]      (the actual consumable — APKT, SEKT, RDMT)
├── SolidCuttingTool  (one-piece — solid carbide endmill, drill)
└── Adapter[]         (interface converters — extensions, reduction sleeves)

Key insight: the insert is the consumable. The body is semi-permanent. The holder is a capital asset. Each has a different cost model and life model.

ToolAssembly cost = sum of all component costs

ToolAssembly life = minimum of all component lives (weakest link)

Component Classification

Component Consumable Tracked
ToolHolder No — capital asset Asset ID
ColletChuck Wear item Replacement interval
CutterBody (insert) No — semi-permanent Asset ID
SolidCuttingTool Yes Type consumption rate
Insert Yes — primary consumable Type consumption rate
Adapter No Asset ID

Why IToolComponent is abstract

Every component shares: cost, manufacturer, quantity, life, consumable status.

But each answers getLife() differently:

  • Insert: edges × life per edge
  • SolidTool: fixed expected life
  • ToolHolder: remaining cycles × average cycle time

IToolComponent declares getLife() as pure virtual (= 0). Every subclass must implement it. The base class cannot — it does not know what type it is.

Life in Minutes, Cycles for Operators

Internally all life is in Minutes — the cost and optimization engine works in one unit.

Externally operators see Cycles — what makes sense on the shop floor.

ToolHolder::getLife() converts: remainingCycles × avgCycleTime = Minutes

ToolHolder::getRemainingCycles() gives the operator-facing view.

4.2 Machine Domain

MachineToolAsset (abstract base)
├── MillingMachine
├── TurningMachine
└── MillTurnMachine   ← third category, eligible for both milling and turning ops

Each machine has a ProcessProfile — instance-level capability, not class-level:

struct ProcessProfile {
    CoolingType    coolingType;    // flood, dry, MQL, mist
    OperationPhase operationPhase; // soft, hard, finishing
    MaterialGroup  materialGroup;  // pre-hardened, hardened, aluminium, cast iron
};

Why ProcessProfile is on the instance, not the class:

Two identical Mazak QT350 lathes — same make, model, specs. One runs soft turning with coolant. One runs hard turning dry. Same class, different ProcessProfile. Without this, the system cannot distinguish them and will assign hard parts to the coolant machine.

Class defines what a machine is. Instance data defines what a machine does in your shop.

4.3 Product Domain

Product
├── partNumber, revision, material
├── monthlyDemand, batchSize
│
├── Routing[]               ← ordered manufacturing steps
│   └── Operation
│       ├── sequenceNumber  (10, 20, 30...)
│       ├── description
│       ├── machineType     (Milling, Turning, MillTurn, External)
│       ├── cycleTime
│       └── phase           (soft, hard, external)
│
└── ToolingPackage[]        ← tool assignment per operation per machine
    └── ToolingEntry
        ├── operationSeqRef
        ├── machineID
        └── ToolAssembly*

Why ToolingPackage is separate from Routing:

The tool assembly for an operation depends on the machine it runs on. Op 20 on M0001 (CAT40 spindle) needs a different assembly than Op 20 on M0002 (HSK63 spindle). The Routing defines what needs to be done. The ToolingPackage defines how to do it on each specific machine.

Why Operations are product-specific:

Even if two products share identical operations, defining operations independently creates confusion at scale. Operations belong to their product's Routing.

Heat treat as an operation:

Heat treat is just another operation with machineType = External. No tooling entry, no machine assignment. Just a step in the routing with a lead time.

batchSize:

Setup time is amortized across the batch. 20-minute setup across 50 parts = 24 seconds per part. Across 5 parts = 4 minutes per part. batchSize is not cosmetic — it directly affects cost per part.

Alternate Routings

One operation can run on multiple machines of the same type. If M0001 is down, op 20 can run on M0003. The OptimizationEngine filters eligible machines by machineType and ProcessProfile, then selects the best available one.


5. Service Layer

CostEngine

cost per part = (machineHourlyRate × cycleTime)
              + (toolCost / toolLife)
              + materialCost
              + (setupCost / batchSize)

Overhead is per machine, not a flat shop-wide percentage.

OptimizationEngine

Given a product and volume:

  1. For each operation, find eligible machines (type match + ProcessProfile match)
  2. For each eligible machine, compute cost per part
  3. Select lowest cost machine that is available

ToolEconomicsEngine

The most important subsystem for practical shop floor decisions:

ToolEconomicsEngine
├── breakEvenCycleTime(oldAssembly, newAssembly, machineID)
│   "New tool costs more — how much faster must it cut to break even?"
│
├── savingsAnalysis(oldAssembly, newAssembly, machineID, monthlyVolume)
│   "New tool is faster and cheaper — exact monthly and annual savings?"
│
├── holderChangeCost(oldAssembly, newAssembly)
│   "Does new tool need a new holder type? If yes, add that capital cost."
│
└── crossShopUsage(toolComponentID)
    "Which products and operations already use this tool?"

Break-even formula:

Total cost per part (old) = Total cost per part (new)

cost per part = (toolCost / toolLife)
              + (machineHourlyRate × cycleTime)
              + (holderCost / holderLife)

Solve for unknown (cycle time reduction needed, or savings achieved).

Holder change cost:

If a new tool requires a different taper type, the holder is a capital cost that must be amortized into the break-even calculation. A new tool that looks cheaper per edge may not be if it requires buying new holders for every machine it runs on.


6. Data Persistence

data/
├── cutting_tools/
│   ├── tool_library.csv       ← master catalog (flat entity data)
│   └── tool_inventory.csv     ← stock levels, reorder points
├── machines/
│   └── machines.csv           ← asset registry
└── products/
    └── products.json          ← part structures (nested — JSON suits hierarchy)

CSV for flat entity tables. JSON for products because the Routing and ToolingPackage are nested structures that CSV cannot represent cleanly.


7. Error Handling: The Result Pattern

Every function that can fail returns Result<T> instead of throwing exceptions or returning sentinel values like -1 or "".

Why not raw exceptions:

double computeCost() {
    if (machineNotFound) throw std::runtime_error("not found");
    return cost;
}

The caller has no idea this can fail unless they read the implementation. They might forget try/catch and the program crashes with no explanation.

Why not sentinel values:

CostPerHour computeCost() {
    if (machineNotFound) return 0.0;
    return cost;
}

Optimizer thinks the machine is free. Picks it as cheapest. Wrong answer, silent bug.

Result forces the caller to handle failure:

Result<CostPerHour> result = computeCost("M0001");

if (result.isOk()) {
    CostPerHour cost = result.value();
} else {
    Error e = result.error();
    // skip this machine, try next
}

The type signature itself tells you the function can fail. The compiler will not let you ignore it.

Error propagation:

Result<Minutes> ToolHolder::getLife() const {
    Result<int> remaining = getRemainingCycles();
    if (remaining.isFail())
        return Result<Minutes>::fail(remaining.error());  // pass error up
    return Result<Minutes>::ok(remaining.value() * m_avgCycleTime);
}

Failures bubble up the call chain automatically. The top layer receives a meaningful error with a code and message, not a crash.

Result internals

template<typename T, typename E = Error>
class Result {
public:
    static Result ok(T value);    // factory — creates success box
    static Result fail(E error);  // factory — creates failure box
    bool isOk()   const;
    bool isFail() const;
    const T& value() const;       // guarded — throws if called on failure
    const E& error() const;       // guarded — throws if called on success
private:
    bool m_ok    = false;
    T    m_value {};
    E    m_error {};
};

static factory methods: ok() and fail() are the only way to create a Result. They are static because their job is to create the object — you cannot call them on an object that does not exist yet.

const T& return: returns a reference to internal data, not a copy. Efficient for large objects. const means caller cannot modify internal data through the reference.

{} initialization: all members start at safe defaults. No uninitialized memory.


8. C++ Concepts Learned So Far

Variables and Types

int      count    = 10;
double   cost     = 42.50;
bool     isReady  = true;
std::string name  = "Mazak";

Type Aliases (using)

using CostPerHour = double;   // same as double but name carries meaning
using AssetID     = std::string;

Enums (enum class)

enum class MachineState { Running, Idle, Down, Maintenance, Setup };
// usage:
MachineState::Running

enum class is strongly typed — compiler prevents comparing MachineState with OperationPhase accidentally.

Struct

A named group of variables. Everything public by default. Used for plain data with no logic:

struct Error {
    std::string code;
    std::string message;
};

Class

Same as struct but everything private by default. Used when you need to control access:

class Result {
public:
    bool isOk() const { return m_ok; }  // exposed
private:
    bool m_ok = false;                  // protected
};

public / protected / private

  • public — anyone can access
  • protected — the class and its subclasses can access, nobody else
  • private — only the class itself can access

Templates

Write once, works for any type:

template<typename T>
class Result {
    T m_value {};  // T is a placeholder — caller fills it in
};

Result<double>       // T = double
Result<ToolAssembly> // T = ToolAssembly

Static Methods

Belong to the class itself, not any object. Called without creating an object first:

Result<double>::ok(42.0);  // no object needed

Used for factory methods whose job is to create the object — you cannot call them on something that does not exist yet.

const on Methods

bool isOk() const { return m_ok; }

Promise: this method will not modify any member data. Compiler enforces it.

const on Return Type

const T& value() const

Two consts, two different jobs:

  • const T& — caller cannot modify what is returned through the reference
  • trailing const — method does not modify the object

Abstract Classes and Pure Virtual Methods

class IToolComponent {
public:
    virtual Result<Minutes> getLife() const = 0;  // pure virtual
};

= 0 means: I am not implementing this. Every subclass must implement it.

Cannot create an object of an abstract class directly — only concrete subclasses.

Inheritance

class ToolHolder : public IToolComponent {
    // inherits m_cost, m_manufacturer, m_quantity automatically
    // must implement getLife(), getType(), isConsumable()
};

override

Result<Minutes> getLife() const override;

Tells compiler: this intentionally overrides a base class method. Compiler errors if the signature does not match — catches typos.

Virtual Destructor

virtual ~IToolComponent() = default;

Required on any class with virtual methods. Without it, deleting a subclass through a base pointer causes memory leaks or undefined behavior.

Declaration vs Definition

// ToolHolder.hpp — declaration
Result<Minutes> getLife() const override;  // semicolon, no body

// ToolHolder.cpp — definition
Result<Minutes> ToolHolder::getLife() const {  // :: connects to class, has body
    // implementation
}

:: is the scope resolution operator — tells the compiler this definition belongs to ToolHolder.

Include Guards

#pragma once  // at top of every .hpp file

Prevents the file being processed twice if included from multiple places. Without it, redefinition errors.

Includes

#include <string>              // system library — angle brackets
#include "mfgos/core/Types.hpp"  // your own files — quotes, full path

Files Written So Far

include/mfgos/core/Types.hpp          — enums, type aliases
include/mfgos/core/Result.hpp         — error handling pattern
include/mfgos/tool/IToolComponent.hpp — abstract base interface
include/mfgos/tool/ToolHolder.hpp     — concrete tool holder class
src/tool/ToolHolder.cpp               — tool holder implementation

What Is Out of Scope for C++

This system is the compute core. The full manufacturing OS vision includes:

Domain Right tool
PFMEA, Control Plan, SWI Document DB + Web UI
CAM files, G-code, schematics File storage + DMS
CMM reports, PPAP data PDF storage + QMS
Real-time machine monitoring C++ — but later
GUI frontend Qt or web-based — later

The C++ library is the engine. Everything else plugs into it.


Next Files

include/mfgos/tool/Insert.hpp
src/tool/Insert.cpp
include/mfgos/tool/SolidCuttingTool.hpp
src/tool/SolidCuttingTool.cpp
include/mfgos/tool/ToolAssembly.hpp
src/tool/ToolAssembly.cpp
include/mfgos/repository/IToolRepository.hpp
src/repository/csv/CSVToolRepository.cpp

About

manufacturingOS

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages