dept-of-scrapyard-robotics / pironman5
Daemon/WebServer for Interacting with the GPIO of a RaspberryPi 5 in a Pironman5(-MAX) case.
Package info
github.com/DeptOfScrapyardRobotics/pironman5
pkg:composer/dept-of-scrapyard-robotics/pironman5
Requires
- php: ^8.3|^8.4
- ext-fd: 0.1.0
- ext-gpio: 0.1.0
- lorisleiva/laravel-actions: dev-main
- projectsaturnstudios/laravel-design-patterns: ^0.1.1
- scrapyard-io/support: 0.3.0
- spatie/laravel-data: ^4.0
Suggests
- laravel/octane: ^2.17
- laravel/reverb: ^1.10
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 serverlaravel/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
idleandtotalare required for every core entry whenprevis 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_0 … core_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.