Browse Source

The first version of the library

master
Sergey Kiselev 4 months ago
parent
commit
e9acd0f187
  1. 38
      examples/test.lua
  2. 40
      src/Makefile
  3. 407
      src/diskinfo.c
  4. 112
      src/smart.h
  5. 159
      test/camtest.c
  6. 154
      test/smartdump.c

38
examples/test.lua

@ -0,0 +1,38 @@
-- test.lua
local diskinfo = require("diskinfo")
local disks, err = diskinfo.get_info()
if not disks then
print("Error fetching disk info: " .. tostring(err))
os.exit(1)
end
print("==================================================================")
print("FREEBSD SYSTEM DISK TREE (With S.M.A.R.T. Temperature)")
print("==================================================================")
for _, disk_name in ipairs(disks._order) do
local disk = disks[disk_name]
-- Проверяем, вернул ли модуль температуру SMART для этого накопителя
local smart_str = ""
if disk.smart_temp then
smart_str = string.format(" | Temp: %d°C", disk.smart_temp)
end
print(string.format("Drive: %s [%s] | Type: %s%s", disk_name, disk.size, disk.type, smart_str))
print(string.format(" |- Hardware Model : %s", disk.model))
print(" |- Partition Tree :")
if #disk.mounts._order == 0 then
print(" [No active mounts discovered on this device]")
else
for _, part_name in ipairs(disk.mounts._order) do
local partition = disk.mounts[part_name]
print(string.format(" -> %-10s | Path: %-20s | FS Type: %s",
part_name, partition.path, partition.type))
end
end
print(string.rep("-", 66))
end

40
src/Makefile

@ -0,0 +1,40 @@
LIB_NAME= diskinfo
SRC= diskinfo.c
PREFIX?= /usr/local
OS!= uname -s
.if ${OS} != "FreeBSD"
.error "This Makefile and module are designed strictly for FreeBSD."
.endif
.if !empty(LUA_VER)
.if ${LUA_VER} == "jit"
LUA_PC= luajit
.else
LUA_PC= lua-${LUA_VER}
.endif
.else
LUA_PC!= pkg-config --list-all | grep -E '^(lua-[0-9]|luajit)' | awk '{print $$1}' | sort -V | tail -n 1
.endif
LUA_CFLAGS!= pkg-config --cflags ${LUA_PC}
LUA_MODDIR!= pkg-config --variable=INSTALL_CMOD ${LUA_PC}
CFLAGS+= -O2 -fPIC -Wall ${LUA_CFLAGS}
LDFLAGS+= -shared -lcam
all: ${LIB_NAME}.so
${LIB_NAME}.so: ${SRC}
${CC} ${CFLAGS} ${LDFLAGS} -o ${LIB_NAME}.so ${SRC}
clean:
rm -f ${LIB_NAME}.so
install: all
mkdir -p ${DESTDIR}${LUA_MODDIR}
install -m 755 ${LIB_NAME}.so ${DESTDIR}${LUA_MODDIR}/${LIB_NAME}.so
.PHONY: all clean install

407
src/diskinfo.c

@ -0,0 +1,407 @@
/*
* diskinfo.c - FreeBSD-specific Lua module for gathering disk hardware
* models, capacities, precise drive types, and active filesystem mount points.
*
* This version uses the native FreeBSD libcam library via O_RDWR access.
* Accurately branches protocols between SCSI SAT for USB bridges (da) and
* native ATA commands for direct SATA controllers (ada).
*/
#ifndef __FreeBSD__
#error "This module is FreeBSD-specific and cannot be compiled on this platform."
#endif
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>
#include <sys/sysctl.h>
#include <sys/disk.h>
#include <sys/ioctl.h>
#include <sys/ata.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <ctype.h>
/* FreeBSD CAM (Common Access Method) userland library interfaces */
#include <cam/cam.h>
#include <cam/cam_ccb.h>
#include <cam/scsi/scsi_all.h>
#include <camlib.h>
/* Core Lua API headers */
#include <lua.h>
#include <lauxlib.h>
/* Internal project definitions */
#include "smart.h"
/* Backward compatibility layer macro to support Lua 5.1 and LuaJIT environments */
#if LUA_VERSION_NUM == 501
#define luaL_newlib(L, l) (lua_newtable(L), luaL_register(L, NULL, l))
#endif
#define MAX_DISKS 64
#define MAX_MOUNTS 128
/* Simple structure used to sort array components alphabetically via qsort API */
typedef struct {
char name[MNAMELEN];
} StringKey;
/* Comparison callback function for alphabetical qsort */
static int compare_strings(const void *a, const void *b) {
return strcmp(((StringKey *)a)->name, ((StringKey *)b)->name);
}
/* Cleans up trailing spaces and non-printable junk from SCSI inquiry strings */
static void clean_scsi_str(char *str) {
int i = strlen(str) - 1;
while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0')) {
str[i] = '\0';
i--;
}
}
/* Fixes word-byte-swapping in raw ATA firmware identity blocks */
static void ata_str_fix(char *str, size_t len) {
for (size_t i = 0; i < len - 1; i += 2) {
char tmp = str[i];
str[i] = str[i + 1];
str[i + 1] = tmp;
}
int i = len - 1;
while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0' || str[i] == '\n' || str[i] == '\r')) {
str[i] = '\0';
i--;
}
}
/* Extracts base drive token name from slice/partition maps (e.g. "ada0p1" -> "ada0") */
static void get_base_disk(const char *dev_name, char *base_buf, size_t buf_len) {
strlcpy(base_buf, dev_name, buf_len);
char *p = base_buf;
while (*p && !(*p >= '0' && *p <= '9')) p++; /* Skip leading letters */
while (*p && (*p >= '0' && *p <= '9')) p++; /* Skip index digits */
if (*p) *p = '\0'; /* Chop partition suffixes */
}
/*
* Queries the device using FreeBSD libcam via appropriate protocol.
* For 'da' (USB) devices: Sends SCSI INQUIRY and SAT ATA PASS-THROUGH (16).
* For 'ada' (SATA) devices: Sends native ATA IDENTIFY command.
* Successfully bypasses all controller bugs to read real struct ata_params.
*/
static void query_cam_properties(const char *base_disk, char *model_buf, size_t buf_len, int *is_removable, int *is_ssd, int *pt_failed, int *temp) {
struct cam_device *cam_dev = NULL;
union ccb *ccb = NULL;
struct ata_params *ident_buf = NULL;
struct smart_values *smart_buf = NULL;
uint8_t *inq_buf = NULL;
*is_removable = 0;
*is_ssd = 0;
*pt_failed = 0;
*temp = -1;
/* CRITICAL: Must use O_RDWR for transport-layer commands execution path to unlock */
cam_dev = cam_open_device(base_disk, O_RDWR);
if (cam_dev == NULL) return;
ccb = cam_getccb(cam_dev);
if (ccb == NULL) {
cam_close_device(cam_dev);
return;
}
ident_buf = calloc(1, sizeof(struct ata_params));
smart_buf = calloc(1, sizeof(struct smart_values));
if (ident_buf == NULL || smart_buf == NULL) {
free(ident_buf); free(smart_buf);
cam_freeccb(ccb); cam_close_device(cam_dev);
return;
}
/* --- PROTOCOL BRANCH 1: USB / SCSI Direct Access Devices (da) --- */
if (strncmp(base_disk, "da", 2) == 0) {
inq_buf = calloc(1, sizeof(struct scsi_inquiry_data));
if (inq_buf == NULL) {
free(ident_buf); free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev);
return;
}
/* 1a: SCSI INQUIRY */
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
struct scsi_inquiry_data *inq_data = (struct scsi_inquiry_data *)inq_buf;
cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE,
(uint8_t *)inq_data, SHORT_INQUIRY_LENGTH, SSD_FULL_SIZE, 6, 5000);
ccb->csio.cdb_io.cdb_bytes[0] = INQUIRY;
ccb->csio.cdb_io.cdb_bytes[4] = SHORT_INQUIRY_LENGTH;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
if ((inq_data->device & SID_RMB) != 0) *is_removable = 1;
char vendor[9], product[17];
snprintf(vendor, sizeof(vendor), "%.8s", inq_data->vendor);
snprintf(product, sizeof(product), "%.16s", inq_data->product);
clean_scsi_str(vendor); clean_scsi_str(product);
snprintf(model_buf, buf_len, "%s %s", vendor, product);
}
/* 1b: SCSI SAT ATA PASS-THROUGH (16) -> IDENTIFY */
cam_freeccb(ccb); ccb = cam_getccb(cam_dev);
if (ccb != NULL) {
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE,
(uint8_t *)ident_buf, sizeof(struct ata_params), SSD_FULL_SIZE, 16, 5000);
uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes;
cdb[0] = 0x85; cdb[1] = 4 << 1; cdb[2] = 0x08 | 0x02; cdb[4] = 1; cdb[14] = 0xEC;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
char real_model[41];
memcpy(real_model, ident_buf->model, sizeof(ident_buf->model));
real_model[sizeof(ident_buf->model)] = '\0';
ata_str_fix(real_model, sizeof(real_model));
if (strlen(real_model) > 0) strlcpy(model_buf, real_model, buf_len);
if (ident_buf->media_rotation_rate == 1) *is_ssd = 1;
} else {
*pt_failed = 1;
}
}
/* 1c: SCSI SAT ATA PASS-THROUGH (16) -> SMART VALUES */
if (!(*pt_failed)) {
cam_freeccb(ccb); ccb = cam_getccb(cam_dev);
if (ccb != NULL) {
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE,
(uint8_t *)smart_buf, sizeof(struct smart_values), SSD_FULL_SIZE, 16, 5000);
uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes;
cdb[0] = 0x85; cdb[1] = 4 << 1; cdb[2] = 0x08 | 0x02; cdb[4] = 0xD0; cdb[6] = 1;
cdb[8] = 0x4F; cdb[10] = 0xC2; cdb[14] = 0xB0;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
for (int i = 0; i < 30; i++) {
struct smart_attribute *a = &smart_buf->attr[i];
if (a->id == 194 || a->id == 190) {
*temp = parse_smart_temperature(a);
break;
}
}
}
}
}
free(inq_buf);
}
/* --- PROTOCOL BRANCH 2: Native SATA/ATA Devices (ada) --- */
else if (strncmp(base_disk, "ada", 3) == 0) {
/* 2a: Pure ATA IDENTIFY */
memset(&ccb->ataio, 0, sizeof(struct ccb_ataio));
ccb->ccb_h.func_code = XPT_ATA_IO; ccb->ccb_h.flags = CAM_DIR_IN; ccb->ccb_h.timeout = 5000;
ccb->ataio.data_ptr = (uint8_t *)ident_buf; ccb->ataio.dxfer_len = sizeof(struct ata_params);
ccb->ataio.cmd.command = 0xEC;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
char real_model[41];
memcpy(real_model, ident_buf->model, sizeof(ident_buf->model));
real_model[sizeof(ident_buf->model)] = '\0';
ata_str_fix(real_model, sizeof(real_model));
if (strlen(real_model) > 0) strlcpy(model_buf, real_model, buf_len);
if (ident_buf->media_rotation_rate == 1) *is_ssd = 1;
}
/* 2b: Pure ATA SMART VALUES */
cam_freeccb(ccb); ccb = cam_getccb(cam_dev);
if (ccb != NULL) {
memset(&ccb->ataio, 0, sizeof(struct ccb_ataio));
ccb->ccb_h.func_code = XPT_ATA_IO; ccb->ccb_h.flags = CAM_DIR_IN; ccb->ccb_h.timeout = 5000;
ccb->ataio.data_ptr = (uint8_t *)smart_buf; ccb->ataio.dxfer_len = sizeof(struct smart_values);
ccb->ataio.cmd.command = 0xB0; ccb->ataio.cmd.features = 0xD0;
ccb->ataio.cmd.lba_mid = 0x4F; ccb->ataio.cmd.lba_high = 0xC2;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
for (int i = 0; i < 30; i++) {
struct smart_attribute *a = &smart_buf->attr[i];
if (a->id == 194 || a->id == 190) {
*temp = parse_smart_temperature(a);
break;
}
}
}
}
}
free(ident_buf); free(smart_buf);
if (ccb != NULL) cam_freeccb(ccb);
cam_close_device(cam_dev);
}
/* Fetches the hardware description/model, raw storage capacity and precise drive type */
static void get_disk_properties(lua_State *L, const char *base_disk) {
char path[MAXPATHLEN];
char model_buf[256];
char type_buf[16];
off_t mediasize = 0;
snprintf(path, sizeof(path), "/dev/%s", base_disk);
strlcpy(model_buf, "Generic Drive", sizeof(model_buf));
strlcpy(type_buf, "unknown", sizeof(type_buf));
if (strncmp(base_disk, "nvme", 4) == 0 || strncmp(base_disk, "nvd", 3) == 0) {
strlcpy(type_buf, "nvme", sizeof(type_buf));
} else if (strncmp(base_disk, "mmcsd", 5) == 0) {
strlcpy(type_buf, "sd", sizeof(type_buf));
}
int fd = open(path, O_RDONLY);
if (fd >= 0) {
struct diocgattr_arg arg;
memset(&arg, 0, sizeof(arg));
strlcpy(arg.name, "GEOM::descr", sizeof(arg.name));
arg.len = sizeof(arg.value.str);
if (ioctl(fd, DIOCGATTR, &arg) == 0 && strlen(arg.value.str) > 0) {
strlcpy(model_buf, arg.value.str, sizeof(model_buf));
}
ioctl(fd, DIOCGMEDIASIZE, &mediasize);
close(fd);
}
int is_ssd = 0, is_removable = 0, pt_failed = 0;
int smart_temp = -1;
if (strncmp(base_disk, "ada", 3) == 0 || strncmp(base_disk, "da", 2) == 0) {
query_cam_properties(base_disk, model_buf, sizeof(model_buf), &is_removable, &is_ssd, &pt_failed, &smart_temp);
}
if (strcmp(type_buf, "unknown") == 0) {
if (strncmp(base_disk, "da", 2) == 0) {
if (is_removable || pt_failed) strlcpy(type_buf, "usb", sizeof(type_buf));
else if (is_ssd) strlcpy(type_buf, "usdssd", sizeof(type_buf));
else strlcpy(type_buf, "usdhdd", sizeof(type_buf));
} else if (strncmp(base_disk, "ada", 3) == 0) {
if (is_ssd) strlcpy(type_buf, "ssd", sizeof(type_buf));
else strlcpy(type_buf, "hdd", sizeof(type_buf));
}
}
/* Push properties into the Lua table current on top of stack */
lua_pushstring(L, model_buf); lua_setfield(L, -2, "model");
lua_pushstring(L, type_buf); lua_setfield(L, -2, "type");
/* Format storage size with binary standards */
char size_buf[32];
double size_gib = (double)mediasize / (1024.0 * 1024.0 * 1024.0);
double final_size = size_gib;
const char *unit = "GiB";
if (size_gib >= 1024.0) { final_size = size_gib / 1024.0; unit = "TiB"; }
if (final_size == (long long)final_size) snprintf(size_buf, sizeof(size_buf), "%lld %s", (long long)final_size, unit);
else snprintf(size_buf, sizeof(size_buf), "%.1f %s", final_size, unit);
lua_pushstring(L, size_buf); lua_setfield(L, -2, "size");
/* Inject smart_temp into Lua fields strictly if it was discovered successfully */
if (smart_temp >= 0) {
lua_pushinteger(L, smart_temp);
lua_setfield(L, -2, "smart_temp");
}
}
/* Main Lua binding function that builds the nested tree structure */
static int lua_get_nested_disks(lua_State *L) {
StringKey disk_names[MAX_DISKS];
int disk_count = 0;
memset(disk_names, 0, sizeof(disk_names));
/* Read the list of available physical disks from sysctl kernel tree */
char *disks_str = NULL; size_t len = 0;
if (sysctlbyname("kern.disks", NULL, &len, NULL, 0) == 0) {
disks_str = malloc(len);
if (disks_str && sysctlbyname("kern.disks", disks_str, &len, NULL, 0) == 0) {
char *token = strtok(disks_str, " ");
while (token != NULL && disk_count < MAX_DISKS) {
strlcpy(disk_names[disk_count].name, token, sizeof(disk_names[disk_count].name));
disk_count++; token = strtok(NULL, " ");
}
}
free(disks_str);
}
qsort(disk_names, disk_count, sizeof(StringKey), compare_strings);
/* Fetch the active kernel mount points table using getfsstat */
struct statfs *mntbuf = NULL;
int num_mounts = getfsstat(NULL, 0, MNT_NOWAIT);
if (num_mounts > 0) {
size_t bufsize = (num_mounts + 4) * sizeof(struct statfs);
mntbuf = malloc(bufsize);
if (mntbuf) num_mounts = getfsstat(mntbuf, bufsize, MNT_NOWAIT);
}
/* Create the main root Lua table and its accompanying order tracker */
lua_newtable(L); lua_newtable(L);
for (int i = 0; i < disk_count; i++) {
lua_pushstring(L, disk_names[i].name); lua_rawseti(L, -2, i + 1);
}
lua_setfield(L, -2, "_order");
/* Populate metrics, sub-tables and partitions for each physical disk discovered */
for (int i = 0; i < disk_count; i++) {
char *current_disk = disk_names[i].name;
lua_newtable(L);
get_disk_properties(L, current_disk);
lua_newtable(L);
StringKey part_names[MAX_MOUNTS]; int part_count = 0;
if (mntbuf && num_mounts > 0) {
for (int m = 0; m < num_mounts; m++) {
char *dev_name = mntbuf[m].f_mntfromname;
if (strncmp(dev_name, "/dev/", 5) == 0) dev_name += 5;
char base_disk[MNAMELEN]; get_base_disk(dev_name, base_disk, sizeof(base_disk));
if (strcmp(base_disk, current_disk) == 0 && part_count < MAX_MOUNTS) {
strlcpy(part_names[part_count].name, dev_name, sizeof(part_names[part_count].name));
part_count++;
}
}
}
qsort(part_names, part_count, sizeof(StringKey), compare_strings);
/* Create inner partition index arrays */
lua_newtable(L);
for (int p = 0; p < part_count; p++) {
lua_pushstring(L, part_names[p].name); lua_rawseti(L, -2, p + 1);
}
lua_setfield(L, -2, "_order");
/* Intersect active mount parameters into final sub-objects */
for (int p = 0; p < part_count; p++) {
for (int m = 0; m < num_mounts; m++) {
char *dev_name = mntbuf[m].f_mntfromname;
if (strncmp(dev_name, "/dev/", 5) == 0) dev_name += 5;
if (strcmp(dev_name, part_names[p].name) == 0) {
lua_newtable(L);
lua_pushstring(L, mntbuf[m].f_mntonname); lua_setfield(L, -2, "path");
lua_pushstring(L, mntbuf[m].f_fstypename); lua_setfield(L, -2, "type");
lua_setfield(L, -2, part_names[p].name); break;
}
}
}
lua_setfield(L, -2, "mounts"); lua_setfield(L, -2, current_disk);
}
if (mntbuf) free(mntbuf);
return 1;
}
/* Official Lua Shared Library registration hooks definition */
int luaopen_diskinfo(lua_State *L) {
static const struct luaL_Reg mylib[] = {
{ "get_info", lua_get_nested_disks },
{ NULL, NULL }
};
luaL_newlib(L, mylib);
return 1;
}

112
src/smart.h

@ -0,0 +1,112 @@
/*
* smart.h - Shared ATA S.M.A.R.T. structures and attribute definitions
* for diskinfo module and smartdump utility.
*
* Supports cross-vendor parsing for HDD (including Helium drives),
* SATA SSD, and Enterprise storage controllers.
*/
#ifndef _DISKINFO_SMART_H_
#define _DISKINFO_SMART_H_
#include <stdint.h>
#pragma pack(push, 1)
/* 12-byte layout of an individual ATA S.M.A.R.T. attribute */
struct smart_attribute {
uint8_t id; /* Attribute ID */
uint16_t status; /* Status flags */
uint8_t current; /* Normalized Value */
uint8_t worst; /* Worst Value */
uint8_t raw[6]; /* 48-bit Raw Value register matrix */
uint8_t reserv;
};
/* 512-byte payload sector returned by SMART READ DATA (0xD0) */
struct smart_values {
uint16_t rev; /* Structure revision */
struct smart_attribute attr[30]; /* 30 sequential attributes */
uint8_t reserved[149];/* Vendor specific padding bytes */
uint8_t chksum; /* Data integrity checksum */
};
#pragma pack(pop)
/*
* Universal and safe cross-vendor temperature parsing rule.
* Grabs the first raw byte for Seagate's noisy multi-byte registers
* and reads the full 48-bit value for Toshiba/WD/Samsung/Intel,
* safely filtering out vendor-specific debug telemetry.
*/
static inline int parse_smart_temperature(struct smart_attribute *a) {
uint64_t raw_val = 0;
for (int b = 0; b < 6; b++) {
raw_val |= ((uint64_t)a->raw[b]) << (b * 8);
}
/* If the full 48-bit value is reasonable, return it directly */
if (raw_val > 0 && raw_val < 150) {
return (int)raw_val;
}
/* Fallback for Seagate: current real temperature is always in the first byte */
return (int)a->raw[0];
}
/*
* Universal ATA/ATAPI SMART Attribute dictionary compliant with T13 standard.
* Declared as static inline to safely include across multiple compilation units.
*/
static inline const char *get_attr_name(uint8_t id) {
switch (id) {
case 1: return "Raw_Read_Error_Rate";
case 2: return "Throughput_Performance";
case 3: return "Spin_Up_Time";
case 4: return "Start_Stop_Count";
case 5: return "Reallocated_Sector_Ct";
case 7: return "Seek_Error_Rate";
case 8: return "Seek_Time_Performance";
case 9: return "Power_On_Hours";
case 10: return "Spin_Retry_Count";
case 12: return "Power_Cycle_Count";
case 16: return "Helium_Level_WD";
case 22: return "Current_Helium_Level"; /* HGST / Hitachi Helium Drives */
case 168: return "SATA_Phy_Error_Count";
case 170: return "Bad_Block_Count_SSD";
case 171: return "Program_Fail_Count_SSD";
case 172: return "Erase_Fail_Count_SSD";
case 173: return "Wear_Leveling_Count"; /* Universal SSD Wear Counter */
case 174: return "Unexpected_Power_Loss";
case 175: return "Power_Loss_Protect_Fail"; /* Intel SSD Capacitor Monitor */
case 177: return "Wear_Range_Delta";
case 179: return "Used_Rsvd_Blk_Cnt_Tot";
case 181: return "Program_Fail_Count_SSD";
case 182: return "Erase_Fail_Count_SSD";
case 183: return "SATA_Downshift_Error_Ct";
case 184: return "End-to-End_Error";
case 187: return "Reported_Uncorrect";
case 188: return "Command_Timeout";
case 189: return "High_Fly_Writes";
case 190: return "Airflow_Temperature_Cel";
case 192: return "Power-Off_Retract_Count";
case 193: return "Load_Cycle_Count";
case 194: return "Temperature_Celsius";
case 195: return "Hardware_ECC_Recovered";
case 196: return "Reallocation_Event_Ct";
case 197: return "Current_Pending_Sector"; /* Crucial HDD Health Marker */
case 198: return "Offline_Uncorrectable";
case 199: return "UDMA_CRC_Error_Count"; /* Interface/Cable Error Counter */
case 202: return "Percentage_Used_Life"; /* SSD Remaining Life % */
case 218: return "SATA_CRC_Error_Count";
case 231: return "SSD_Life_Left";
case 232: return "Available_Reserv_Space"; /* SSD Available Reserved Flash % */
case 233: return "Media_Wearout_Indicator";
case 235: return "POR_Recovery_Count"; /* Samsung SSD Panic Recovery Count */
case 240: return "Head_Flying_Hours";
case 241: return "Total_LBAs_Written"; /* NAND TBW Metric */
case 242: return "Total_LBAs_Read"; /* Host Reads Metric */
case 244: return "Thermal_Throttle_Count"; /* SSD Overheating Throttling Events */
default: return "Vendor_Specific";
}
}
#endif /* _DISKINFO_SMART_H_ */

159
test/camtest.c

@ -0,0 +1,159 @@
/*
* cc -O2 -Wall camtest.c -lcam -o camtest
*/
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/disk.h>
#include <sys/ata.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
/* Official FreeBSD CAM Library Interface Headers */
#include <camlib.h>
#include <cam/scsi/scsi_all.h>
static void trim_str(char *str) {
int i = strlen(str) - 1;
while (i >= 0 && (isspace((unsigned char)str[i]) || str[i] == '\0')) {
str[i] = '\0';
i--;
}
}
static void ata_str_fix(char *str, size_t len) {
for (size_t i = 0; i < len - 1; i += 2) {
char tmp = str[i];
str[i] = str[i + 1];
str[i + 1] = tmp;
}
trim_str(str);
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <device_name> (e.g. ada0 or da0)\n", argv[0]);
return 1;
}
const char *disk = argv[1];
printf("=== Testing device via libcam (O_RDWR): %s ===\n\n", disk);
/* -------------------------------------------------------------
* TEST 1: Open device via libcam using O_RDWR
* ------------------------------------------------------------- */
struct cam_device *cam_dev = cam_open_device(disk, O_RDWR);
if (cam_dev == NULL) {
fprintf(stderr, "Error: cam_open_device failed for '%s'.\n", disk);
fprintf(stderr, "Check permissions on /dev/xpt0 and /dev/pass* (group operator/devfs.rules).\n");
return 1;
}
union ccb *ccb = cam_getccb(cam_dev);
if (ccb == NULL) {
fprintf(stderr, "Error: Failed to allocate CCB block\n");
cam_close_device(cam_dev);
return 1;
}
/* Allocate 512 bytes alignment buffer for responses */
uint8_t *data_buf = calloc(1, 512);
if (data_buf == NULL) {
fprintf(stderr, "Error: Memory allocation failed\n");
cam_freeccb(ccb);
cam_close_device(cam_dev);
return 1;
}
/* -------------------------------------------------------------
* TEST 2: Pure SCSI INQUIRY
* ------------------------------------------------------------- */
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
struct scsi_inquiry_data *inq = (struct scsi_inquiry_data *)data_buf;
cam_fill_csio(&ccb->csio,
/*retries*/ 2,
/*cbfcnp*/ NULL,
/*flags*/ CAM_DIR_IN,
/*tag_action*/ CAM_TAG_ACTION_NONE,
/*data_ptr*/ (uint8_t *)inq,
/*dxfer_len*/ SHORT_INQUIRY_LENGTH,
/*sense_len*/ SSD_FULL_SIZE,
/*cdb_len*/ 6,
/*timeout*/ 5000);
/* Safely fill raw SCSI INQUIRY command bytes */
ccb->csio.cdb_io.cdb_bytes[0] = INQUIRY;
ccb->csio.cdb_io.cdb_bytes[4] = SHORT_INQUIRY_LENGTH;
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
char vendor[9], product[17];
snprintf(vendor, sizeof(vendor), "%.8s", inq->vendor);
snprintf(product, sizeof(product), "%.16s", inq->product);
trim_str(vendor); trim_str(product);
printf("[SCSI INQUIRY]:\n");
printf(" Vendor : %s\n", vendor);
printf(" Product : %s\n", product);
printf(" Removable : %s (RMB Bit)\n", (inq->device & SID_RMB) ? "Yes (Flash/SD)" : "No (Fixed/HDD/SSD)");
} else {
printf("[SCSI INQUIRY]: Failed (CAM Status: 0x%X)\n", ccb->ccb_h.status);
}
/* -------------------------------------------------------------
* TEST 3: SCSI SAT ATA PASS-THROUGH (16) -> ATA IDENTIFY
* ------------------------------------------------------------- */
cam_freeccb(ccb);
ccb = cam_getccb(cam_dev);
if (ccb == NULL) {
free(data_buf);
cam_close_device(cam_dev);
return 1;
}
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
memset(data_buf, 0, 512);
struct ata_params *ident_buf = (struct ata_params *)data_buf;
cam_fill_csio(&ccb->csio,
/*retries*/ 2,
/*cbfcnp*/ NULL,
/*flags*/ CAM_DIR_IN,
/*tag_action*/ CAM_TAG_ACTION_NONE,
/*data_ptr*/ (uint8_t *)ident_buf,
/*dxfer_len*/ sizeof(struct ata_params),
/*sense_len*/ SSD_FULL_SIZE,
/*cdb_len*/ 16,
/*timeout*/ 5000);
/* Construct manual SAT ATA PASS-THROUGH (16) command block array */
uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes;
cdb[0] = 0x85; /* ATA PASS-THROUGH (16) Opcode */
cdb[1] = 4 << 1; /* Protocol: PIO Data-In */
cdb[2] = 0x08 | 0x02; /* T_DIR = 1 (Data In), ByteBlock = 1 (Block mode) */
cdb[4] = 1; /* Sector Count = 1 sector (512 bytes) */
cdb[14] = 0xEC; /* ATA Command: IDENTIFY DEVICE */
if (cam_send_ccb(cam_dev, ccb) == 0 && (ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) {
char ata_model[41];
memcpy(ata_model, ident_buf->model, sizeof(ident_buf->model));
ata_model[40] = '\0';
ata_str_fix(ata_model, sizeof(ata_model));
printf("\n[SCSI ATA PASS-THROUGH (16)]:\n");
printf(" ATA Model : %s\n", ata_model);
printf(" Media RPM : %d (0=SSD, 1=Unknown)\n", ident_buf->media_rotation_rate);
} else {
printf("\n[SCSI ATA PASS-THROUGH (16)]: Failed / rejected by bridge (CAM Status: 0x%X)\n", ccb->ccb_h.status);
}
free(data_buf);
cam_freeccb(ccb);
cam_close_device(cam_dev);
return 0;
}

154
test/smartdump.c

@ -0,0 +1,154 @@
/*
* smartdump.c - FreeBSD CLI utility for querying and decoding
* disk hardware S.M.A.R.T. records directly via the CAM subsystem.
*
* Can output raw binary stream for piping or human-readable decoded matrix.
*
* cc -O2 -Wall smartdump.c -lcam -o smartdump
*/
#include <sys/param.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
#include <stdint.h>
/* FreeBSD CAM Library Interface Headers */
#include <camlib.h>
#include <cam/scsi/scsi_all.h>
/* Import local shared SMART layout matrix definitions */
#include "../src/smart.h"
/* Cross-vendor SMART buffer decoder and normalizer */
static void decode_smart(struct smart_values *smart_buf) {
printf("ID# ATTRIBUTE_NAME VALUE WORST RAW_VALUE\n");
printf("==================================================================\n");
for (int i = 0; i < 30; i++) {
struct smart_attribute *a = &smart_buf->attr[i];
if (a->id == 0) continue;
uint64_t raw_val = 0;
/* Enforce custom cross-vendor temperature extraction rule */
if (a->id == 194 || a->id == 190) {
raw_val = parse_smart_temperature(a);
} else {
/* Assemble clean 48-bit metric integer for standard counters */
for (int b = 0; b < 6; b++) {
raw_val |= ((uint64_t)a->raw[b]) << (b * 8);
}
}
printf("%3d %-23s %3d %3d %llu\n",
a->id, get_attr_name(a->id), a->current, a->worst, (unsigned long long)raw_val);
}
}
int main(int argc, char **argv) {
int ch;
int mode_decode = 0;
/* Parse configuration operational runtime flag args */
while ((ch = getopt(argc, argv, "dr")) != -1) {
switch (ch) {
case 'd': mode_decode = 1; break;
case 'r': mode_decode = 0; break;
default:
fprintf(stderr, "Usage: %s [-d|-r] <device_name>\n", argv[0]);
return 1;
}
}
argc -= optind;
argv += optind;
if (argc < 1) {
fprintf(stderr, "Usage: %s [-d|-r] <device_name> (e.g. ada1 or da0)\n", argv[-optind]);
return 1;
}
const char *disk = argv[0];
struct cam_device *cam_dev = NULL;
union ccb *ccb = NULL;
struct smart_values *smart_buf = NULL;
/* Open device via O_RDWR to unlock transport-layer CCB execution paths */
cam_dev = cam_open_device(disk, O_RDWR);
if (cam_dev == NULL) {
fprintf(stderr, "Error: cam_open_device failed for '%s'. Check permissions.\n", disk);
return 1;
}
ccb = cam_getccb(cam_dev);
if (ccb == NULL) {
fprintf(stderr, "Error: Failed to allocate CCB block\n");
cam_close_device(cam_dev);
return 1;
}
smart_buf = calloc(1, sizeof(struct smart_values));
if (smart_buf == NULL) {
fprintf(stderr, "Error: Memory allocation failed\n");
cam_freeccb(ccb);
cam_close_device(cam_dev);
return 1;
}
/* --- PROTOCOL BRANCH 1: Native SATA/ATA Devices (ada) --- */
if (strncmp(disk, "ada", 3) == 0) {
cam_fill_ataio(&ccb->ataio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE,
(uint8_t *)smart_buf, sizeof(struct smart_values), 5000);
ccb->ataio.cmd.command = 0xB0; /* ATA SMART Command Opcode */
ccb->ataio.cmd.features = 0xD0; /* SMART READ DATA Command Feature Code */
ccb->ataio.cmd.lba_mid = 0x4F; /* Subsystem Spec Signature Marker */
ccb->ataio.cmd.lba_high = 0xC2; /* Subsystem Spec Signature Marker */
if (cam_send_ccb(cam_dev, ccb) != 0 || (ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) {
fprintf(stderr, "Error: ATA SMART command failed (CAM Status: 0x%X)\n", ccb->ccb_h.status);
free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1;
}
}
/* --- PROTOCOL BRANCH 2: USB / SCSI Bridges (da) --- */
else if (strncmp(disk, "da", 2) == 0) {
memset(&ccb->csio, 0, sizeof(struct ccb_scsiio));
cam_fill_csio(&ccb->csio, 2, NULL, CAM_DIR_IN, CAM_TAG_ACTION_NONE,
(uint8_t *)smart_buf, sizeof(struct smart_values), SSD_FULL_SIZE, 16, 5000);
uint8_t *cdb = ccb->csio.cdb_io.cdb_bytes;
cdb[0] = 0x85; /* SCSI Opcode: ATA PASS-THROUGH (16) */
cdb[1] = 4 << 1; /* Protocol: PIO Data-In */
cdb[2] = 0x08 | 0x02; /* T_DIR = 1 (Data-In), ByteBlock = 1 (Sectors mode) */
cdb[4] = 0xD0; /* Features (lower) -> SMART READ DATA */
cdb[6] = 1; /* Sector Count = 1 sector (512 bytes) */
cdb[8] = 0x4F; /* LBA Mid -> SMART Signature Mid */
cdb[10] = 0xC2; /* LBA High -> SMART Signature High */
cdb[14] = 0xB0; /* ATA Command -> SMART Command Opcode */
if (cam_send_ccb(cam_dev, ccb) != 0 || (ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) {
fprintf(stderr, "Error: SCSI SAT SMART command failed (CAM Status: 0x%X)\n", ccb->ccb_h.status);
free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1;
}
} else {
fprintf(stderr, "Error: Unsupported device driver type target routing path.\n");
free(smart_buf); cam_freeccb(ccb); cam_close_device(cam_dev); return 1;
}
/* Route output generation maps according to active selection flag switches */
if (mode_decode) {
decode_smart(smart_buf);
} else {
fwrite(smart_buf, 1, 512, stdout);
}
free(smart_buf);
cam_freeccb(ccb);
cam_close_device(cam_dev);
return 0;
}
Loading…
Cancel
Save