Daemon/WebServer for Interacting with the GPIO of a RaspberryPi 5 in a Pironman5(-MAX) case.

Maintainers

Package info

github.com/DeptOfScrapyardRobotics/pironman5

Homepage

pkg:composer/dept-of-scrapyard-robotics/pironman5

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.0 2026-04-17 05:40 UTC

This package is auto-updated.

Last update: 2026-04-17 05:41:14 UTC


README

A Laravel package that exposes a real-time system-stats API for a Raspberry Pi 5 enclosed in a Pironman5 or Pironman5 MAX case. The API is served by Laravel Octane and is accessible both locally and over your LAN. A built-in daemon service layer continuously samples stats, diffs them against the previous reading, and appends changes to per-subsystem CSV logs.

Hardware Context

Component Details
SBC Raspberry Pi 5
Case Pironman5 / Pironman5 MAX
Boot drive NVMe via PCIe (up to 2 drives on MAX)
SD card slot Onboard — readable even when not booted from SD
USB storage Any attached USB storage devices

The package understands the Pi 5 block-device layout (nvme*, mmcblk*, sd*) and classifies each disk automatically.

Requirements

Dependency Version
PHP ^8.3 | ^8.4
Laravel ^11 | ^12 | ^13
ext-fd 0.1.0
ext-gpio 0.1.0
scrapyard-io/support 0.3.0
lorisleiva/laravel-actions dev-main
spatie/laravel-data ^4.0

Suggested (strongly recommended):

  • laravel/octane ^2.17 — high-performance application server
  • laravel/reverb ^1.10 — WebSocket broadcasting for live stat streaming

Installation

composer require dept-of-scrapyard-robotics/pironman5

The package auto-discovers its service provider via Laravel's package discovery.

Publishing the config

php artisan vendor:publish --tag=pironman5

This copies config/pironman5.php into your application's config/ directory.

php artisan vendor:publish --tag=pironman5 --force   # overwrite an existing published config

After publishing, edit config/pironman5.php to configure middleware, toggle Octane/Reverb support, set log paths, etc.

Configuration

config/pironman5.php

use DeptOfScrapyardRobotics\Pironman5\Services\Daemon\SystemMonitorService;

return [
    // Set to true when running under Laravel Octane
    'octane' => false,

    // Set to true when Laravel Reverb is installed for WebSocket broadcasting
    'reverb' => false,

    // Middleware applied to all /pironman5/* API routes.
    // Defaults to an empty array (no middleware). Add 'auth:sanctum', throttle rules, etc.
    'api_middleware' => [],

    // Microseconds to sleep at the end of each daemon loop iteration (~60 Hz by default).
    'daemon_eow_delay' => ((1 / 60) * 1000),

    // Daemon service definitions — each entry is instantiated and driven by the daemon loop.
    'services' => [
        'system_monitor' => [
            'class' => SystemMonitorService::class,
            'settings' => [
                'logs' => [
                    'cpu' => storage_path('cpu_log.csv'),
                    'gpu' => storage_path('gpu_log.csv'),
                    'hdd' => storage_path('hdd_log.csv'),
                    'ram' => storage_path('ram_log.csv'),
                ],
            ],
        ],
    ],
];

Serving the API

The package is designed to run under Laravel Octane, which keeps the application in memory for near-zero latency stat reads.

Start Octane (FrankenPHP or Swoole)

php artisan octane:start --port=8000

Access the API

Context Base URL
On the Pi itself http://127.0.0.1:8000/pironman5
From your LAN http://192.168.x.x:8000/pironman5

All routes are prefixed with /api/pironman5/system/stats/.

Running at Startup (systemd)

The package ships two Artisan commands that manage a systemd service unit, so the daemon starts automatically on boot.

Register the service

Run the following from your Laravel project root on the Pi:

sudo php artisan pironman:service:register

The command auto-detects the PHP binary, artisan path, and current OS user, then shows you the generated unit file before asking for confirmation. You can override any value:

sudo php artisan pironman:service:register \
    --service-name=pironman5 \
    --user=angel \
    --php=/usr/bin/php8.4

Pass --start to also start the service immediately:

sudo php artisan pironman:service:register --start

This is equivalent to:

# What the command does internally:
sudo tee /etc/systemd/system/pironman5.service << 'EOF'
[Unit]
Description=Pironman5 Daemon
After=network.target

[Service]
Type=simple
User=angel
WorkingDirectory=/home/angel/Development/PHP/pironman5
ExecStart=/usr/bin/php /home/angel/Development/PHP/pironman5/artisan pironman:go
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable pironman5
sudo systemctl start pironman5

Remove the service

sudo php artisan pironman:service:unregister

This stops the running service (if active), disables it, removes the unit file, and reloads systemd. Use --force to skip the confirmation prompt:

sudo php artisan pironman:service:unregister --force

Manage the service manually

sudo systemctl start pironman5    # start now
sudo systemctl stop pironman5     # stop now
sudo systemctl restart pironman5  # restart
sudo systemctl status pironman5   # check status + recent log lines
journalctl -u pironman5 -f        # follow live output

API Reference

All endpoints return JSON. No authentication is applied by default — add middleware via the api_middleware config key.

CPU

GET /api/pironman5/system/stats/cpu/load

Returns the average CPU load and per-core load percentages calculated as a delta between two /proc/stat reads. On the first call omit prev — the response will return cpu_load: 0.0 and an empty cpu_cores array along with a prev snapshot. Pass that snapshot back on every subsequent call to receive real load figures.

Route name: pironman5.system.stats.cpu.loads

Request parameters:

Parameter Rule Description
prev sometimes|array Previous CPU snapshot returned by the last call. Omit on first poll.
prev.<core>.idle required_with:prev|integer|min:0 Idle+iowait tick counter for the named core (e.g. prev[cpu0][idle]).
prev.<core>.total required_with:prev|integer|min:0 Total tick counter for the named core (e.g. prev[cpu0][total]).

Both idle and total are required for every core entry when prev is present; passing a partial snapshot will fail validation.

First call (no prev):

GET /api/pironman5/system/stats/cpu/load
{
    "cpu_load": 0.0,
    "cpu_cores": [],
    "prev": {
        "cpu0": { "idle": 123456, "total": 234567 },
        "cpu1": { "idle": 124567, "total": 235678 },
        "cpu2": { "idle": 125678, "total": 236789 },
        "cpu3": { "idle": 126789, "total": 237890 }
    }
}

Subsequent calls (pass prev back):

GET /api/pironman5/system/stats/cpu/load?prev[cpu0][idle]=123456&prev[cpu0][total]=234567&...
{
    "cpu_load": 12.34,
    "cpu_cores": [10.12, 14.56, 11.00, 13.78],
    "prev": {
        "cpu0": { "idle": 124100, "total": 235200 },
        ...
    }
}
Field Type Description
cpu_load float Average load across all cores (0–100)
cpu_cores float[] Per-core load percentage, indexed by core number
prev object Current snapshot — pass this back as prev on the next request

GET /api/pironman5/system/stats/cpu/temp

Returns the CPU die temperature read from /sys/class/thermal/thermal_zone0/temp.

Route name: pironman5.system.stats.cpu.temp

Response:

{
    "temp": 52.312
}
Field Type Description
temp float Temperature in °C

GET /api/pironman5/system/stats/cpu/log

Returns the last N rows from the CPU stat log as an array of objects keyed by CSV column name.

Route name: pironman5.system.stats.cpu.log

Request parameters:

Parameter Rule Description
num_lines sometimes|required|integer|min:1 Number of rows to return. Defaults to 50.

Response:

{
    "log": [
        { "timestamp": "2026-04-16 12:00:01", "cpu_temp": "52.31", "cpu_load": "8.12", "core_0": "7.40", "core_1": "9.20", "core_2": "7.80", "core_3": "8.08" },
        { "timestamp": "2026-04-16 12:00:02", "cpu_temp": "52.43", "cpu_load": "9.55", "core_0": "8.10", "core_1": "11.20", "core_2": "9.40", "core_3": "9.50" }
    ]
}

GPU

GET /api/pironman5/system/stats/gpu/load

Returns the VideoCore GPU utilisation read from /sys/class/devfreq/devfreq0/load.

Route name: pironman5.system.stats.gpu.load

Response:

{
    "load": 4.0
}
Field Type Description
load float GPU load percentage (0–100)

GET /api/pironman5/system/stats/gpu/log

Returns the last N rows from the GPU stat log.

Route name: pironman5.system.stats.gpu.log

Request parameters:

Parameter Rule Description
num_lines sometimes|required|integer|min:1 Number of rows to return. Defaults to 50.

Response:

{
    "log": [
        { "timestamp": "2026-04-16 12:00:01", "gpu_load": "4.0" },
        { "timestamp": "2026-04-16 12:00:02", "gpu_load": "6.0" }
    ]
}

RAM

GET /api/pironman5/system/stats/ram/info

Returns memory statistics parsed from /proc/meminfo.

Route name: pironman5.system.stats.ram.info

Response:

{
    "total_bytes": 8323186688.0,
    "available_bytes": 7516192768.0,
    "used_bytes": 806993920.0,
    "total_gb": 7.75,
    "used_gb": 0.75,
    "percent_used": 9.69
}
Field Type Description
total_bytes float Total installed RAM in bytes
available_bytes float Available (not used) RAM in bytes
used_bytes float Used RAM in bytes
total_gb float Total RAM in GiB (2 dp)
used_gb float Used RAM in GiB (2 dp)
percent_used float Percentage used (0–100, 2 dp)

GET /api/pironman5/system/stats/ram/log

Returns the last N rows from the RAM stat log.

Route name: pironman5.system.stats.ram.log

Request parameters:

Parameter Rule Description
num_lines sometimes|required|integer|min:1 Number of rows to return. Defaults to 50.

Response:

{
    "log": [
        { "timestamp": "2026-04-16 12:00:01", "used_bytes": "806993920", "total_bytes": "8323186688", "used_gb": "0.75", "total_gb": "7.75", "percent_used": "9.69" }
    ]
}

HDD / Storage

GET /api/pironman5/system/stats/hdd/disks

Returns all physical block devices known to the kernel, including devices that are not mounted (e.g. an SD card when booting from NVMe). Devices are discovered via /sys/block/ and cross-referenced with /proc/mounts.

Route name: pironman5.system.stats.hdd.disks

Response:

{
    "disks": [
        {
            "device": "/dev/nvme0n1p2",
            "mount_point": "/",
            "fs_type": "ext4",
            "type": "nvme",
            "mounted": true,
            "total_bytes": 1008184320000,
            "used_bytes": 52876881920,
            "free_bytes": 955307438080,
            "total_gb": 938.94,
            "used_gb": 49.25,
            "free_gb": 889.70,
            "percent_used": 5.24
        },
        {
            "device": "/dev/mmcblk0p1",
            "mount_point": null,
            "fs_type": null,
            "type": "sdcard",
            "mounted": false,
            "total_bytes": 31268536320,
            "used_bytes": null,
            "free_bytes": null,
            "total_gb": 29.13,
            "used_gb": null,
            "free_gb": null,
            "percent_used": null
        }
    ]
}

Per-disk fields:

Field Type Description
device string Resolved block device path
mount_point string|null Filesystem mount point, null if not mounted
fs_type string|null Filesystem type (ext4, vfat, …), null if not mounted
type string Hardware class: nvme, sdcard, usb, or unknown
mounted bool Whether the partition is currently mounted
total_bytes int Total partition size in bytes
used_bytes int|null Used bytes — null when not mounted
free_bytes int|null Free bytes — null when not mounted
total_gb float Total size in GiB (2 dp)
used_gb float|null Used in GiB — null when not mounted
free_gb float|null Free in GiB — null when not mounted
percent_used float|null Percentage used — null when not mounted

Device type classification:

type value Device pattern Typical source
nvme nvme* PCIe NVMe SSD (up to 2 on MAX)
sdcard mmcblk* Onboard SD card slot
usb sd[a-z]* USB-attached storage
unknown anything else Other block device

GET /api/pironman5/system/stats/hdd/log

Returns the last N rows from the HDD stat log. Each row represents one disk device at the time it was sampled; multiple disks produce multiple rows per timestamp.

Route name: pironman5.system.stats.hdd.log

Request parameters:

Parameter Rule Description
num_lines sometimes|required|integer|min:1 Number of rows to return. Defaults to 50.

Response:

{
    "log": [
        { "timestamp": "2026-04-16 12:00:01", "device": "/dev/nvme0n1p2", "type": "nvme", "mounted": "1", "total_gb": "938.94", "used_gb": "49.25", "free_gb": "889.70", "percent_used": "5.24" },
        { "timestamp": "2026-04-16 12:00:01", "device": "/dev/mmcblk0p1", "type": "sdcard", "mounted": "0", "total_gb": "29.13", "used_gb": "", "free_gb": "", "percent_used": "" }
    ]
}

Daemon Services

The package includes a daemon service layer designed to be driven by a long-running process (e.g. an Artisan command). Each service implements a three-phase lifecycle per tick:

Phase Method Responsibility
Prepare prepare(array &$shared): ?array Fetches fresh data from the hardware and returns it as $prep_result.
Execute execute(?array $payload): ?array Diffs $payload (new data) against the current class-level state. Returns only keys whose values changed; unchanged keys remain null.
Finish finish(?array $exec_result, ?array $prep_result, array &$shared): static Updates class-level state from $prep_result and writes CSV log entries for any subsystems that reported a change in $exec_result.

SystemMonitorService

Samples CPU load & temperature, GPU load, RAM usage, and disk info on every tick.

use DeptOfScrapyardRobotics\Pironman5\Services\Daemon\SystemMonitorService;

$service = SystemMonitorService::start(
    config('pironman5.services.system_monitor.settings')
);

// Drive the service manually:
$shared   = [];
$prep     = $service->prepare($shared);
$changes  = $service->execute($prep);
$service->finish($changes, $prep, $shared);

Getters:

$service->getCpuTemp();    // float (°C)
$service->getCoreLoads();  // CPUCoreLoads DTO
$service->getGpuLoad();    // float (%)
$service->getMemoryInfo(); // MemoryInfo DTO
$service->getDisks();      // DiskInfo[]

CSV Logging

Each subsystem writes to its own append-only CSV file, configured under services.system_monitor.settings.logs. A header row is written automatically on first creation. Log entries are only appended when execute() detects a change for that subsystem.

Config key Default path Columns
logs.cpu storage/cpu_log.csv timestamp, cpu_temp, cpu_load, core_0core_N
logs.gpu storage/gpu_log.csv timestamp, gpu_load
logs.ram storage/ram_log.csv timestamp, used_bytes, total_bytes, used_gb, total_gb, percent_used
logs.hdd storage/hdd_log.csv timestamp, device, type, mounted, total_gb, used_gb, free_gb, percent_used

Package Structure

src/
├── Actions/
│   ├── Daemon/
│   │   └── StartUp/
│   │       └── StartSystemMonitorService.php  — factory action for SystemMonitorService
│   └── SystemStats/
│       ├── CPU/
│       │   ├── GetCPUCoreLoads.php            — /proc/stat delta-based load
│       │   └── GetCPUTemp.php                 — /sys/class/thermal temperature
│       ├── GPU/
│       │   └── GetGPULoad.php                 — devfreq0 GPU utilisation
│       ├── HDD/
│       │   └── GetMountedDiskInfo.php         — /sys/block + /proc/mounts
│       ├── Log/
│       │   └── ReadStatLog.php                — reads last N rows from a stat CSV
│       └── RAM/
│           └── GetMemoryInfo.php              — /proc/meminfo
├── Console/
│   └── Commands/
│       ├── StartDaemonCommand.php             — pironman:go
│       ├── RegisterDaemonCommand.php          — pironman:service:register
│       └── UnregisterDaemonCommand.php        — pironman:service:unregister
├── Contracts/
│   └── Services/
│       └── Daemon/
│           └── DaemonService.php              — prepare/execute/finish interface
├── DTO/
│   └── SystemStats/
│       ├── SystemStats.php                    — abstract base (Spatie Data)
│       ├── CPUCoreLoads.php
│       ├── DiskInfo.php
│       └── MemoryInfo.php
├── Http/
│   ├── Controllers/
│   │   └── API/
│   │       ├── CPUStatsAPIController.php
│   │       ├── GPUStatsAPIController.php
│   │       ├── HDDStatsAPIController.php
│   │       └── RAMStatsAPIController.php
│   └── Requests/
│       └── API/
│           ├── GetCPUCoreLoadsRequest.php     — validates prev[*][idle|total]
│           └── GetStatLogRequest.php          — validates num_lines
└── Services/
    └── Daemon/
        ├── DaemonService.php                  — abstract base service
        └── SystemMonitorService.php           — CPU/GPU/RAM/HDD monitor + CSV logger

routes/
├── api.php
└── web.php

config/
└── pironman5.php

Actions

All actions use lorisleiva/laravel-actions and can be called via ::run() anywhere in your application, not just through the HTTP layer.

use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\RAM\GetMemoryInfo;
use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\CPU\GetCPUCoreLoads;
use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\CPU\GetCPUTemp;
use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\GPU\GetGPULoad;
use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\HDD\GetMountedDiskInfo;
use DeptOfScrapyardRobotics\Pironman5\Actions\SystemStats\Log\ReadStatLog;

$memory  = GetMemoryInfo::run();                          // MemoryInfo DTO
$cpu     = GetCPUCoreLoads::run($prev);                   // CPUCoreLoads DTO
$temp    = GetCPUTemp::run();                             // float (°C)
$gpu     = GetGPULoad::run();                             // float (%)
$disks   = GetMountedDiskInfo::run();                     // DiskInfo[]
$rows    = ReadStatLog::run(storage_path('cpu_log.csv')); // array<int, array<string, string>>

DTOs

All DTOs extend DeptOfScrapyardRobotics\Pironman5\DTO\SystemStats\SystemStats, which itself extends Spatie\LaravelData\Data. This means every DTO supports .toArray(), .toJson(), and is directly JsonSerializable — pass one straight to response()->json() with no extra mapping.

License

MIT — see LICENSE for details.