diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx index 4a34d8d7..445484dd 100644 --- a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx +++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceCard.tsx @@ -1,5 +1,5 @@ import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material"; -import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT } from "../../../queries/equipmentInstanceQueries"; +import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries"; import ActionButton from "../../../common/ActionButton"; import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline'; import { useMutation, useQuery } from "@apollo/client/react"; @@ -30,6 +30,11 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps) awaitRefetchQueries: true }); + const [updateInstanceHobbsTime] = useMutation(UPDATE_INSTANCE_HOBBS_TIME, { + refetchQueries: ["EquipmentInstances", "GetUnpairedAccessControllers"], + awaitRefetchQueries: true + }); + const [updatePairing] = useMutation(UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, { refetchQueries: ["EquipmentInstances", "GetUnpairedAccessControllers"], awaitRefetchQueries: true @@ -48,9 +53,11 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps) const [sendCommandedState] = useMutation(COMMAND_CONTROLLER_STATE); const [commandedState, setCommandedState] = useState("IDLE"); + const [hobbsTime, setHobbsTime] = useState(props.instance.hobbsTime); async function handleSave() { setAllowEdit(false); + await updateInstanceHobbsTime({ variables: { id: props.instance.id, hobbsTime: hobbsTime } }) await updateInstance({ variables: { id: props.instance.id, name: name, status: status } }) await updatePairing({ variables: { id: Number(props.instance.id), accessControllerID: pairedController?.id } }) } @@ -93,6 +100,20 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps) />; } + function formatSeconds(allSeconds: number){ + const hours = Math.floor(allSeconds / 3600); + if (hours > 0){ + return (allSeconds / 3600).toFixed(2) + " hrs" + } + const minutes = Math.floor((allSeconds % 3600) / 60); + const seconds = allSeconds % 60; + if (minutes > 0){ + return (minutes + seconds / 50).toFixed(2) + " min" + } else { + return Math.floor(allSeconds) + "sec" + } + } + function activeUserDisplay() { if (!currentAccessController) { return No User; @@ -109,6 +130,15 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps) } + function hobbsTimeSetField(){ + return setHobbsTime(Math.abs(Number(e.target.value)))} + > + + } return ( @@ -131,6 +161,16 @@ export default function EquipmentInstanceCard(props: EquipmentInstanceCardProps) } + + + { + !allowEdit + ? formatSeconds(props.instance.hobbsTime) + : hobbsTimeSetField() + } + + + { !allowEdit diff --git a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx index 5fb857f1..83c298dd 100644 --- a/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx +++ b/client/src/pages/makerspace_page/equipment_pages/EquipmentInstanceRow.tsx @@ -1,5 +1,5 @@ import { Autocomplete, Button, Card, Chip, IconButton, Link, MenuItem, Select, Stack, TextField, Typography } from "@mui/material"; -import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT } from "../../../queries/equipmentInstanceQueries"; +import { DELETE_EQUIPMENT_INSTANCE, EquipmentInstance, InstanceStatus, UPDATE_INSTANCE, UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, UPDATE_INSTANCE_HOBBS_TIME } from "../../../queries/equipmentInstanceQueries"; import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline'; import { useMutation, useQuery } from "@apollo/client/react"; import { useState } from "react"; @@ -29,6 +29,11 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) { awaitRefetchQueries: true }); + const [updateInstanceHobbsTime] = useMutation(UPDATE_INSTANCE_HOBBS_TIME, { + refetchQueries: ["EquipmentInstances", "GetUnpairedAccessControllers"], + awaitRefetchQueries: true + }); + const [updatePairing] = useMutation(UPDATE_INSTANCE_CONTROLLER_ASSIGNMENT, { refetchQueries: ["EquipmentInstances", "GetUnpairedAccessControllers"], awaitRefetchQueries: true @@ -47,9 +52,12 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) { const [sendCommandedState] = useMutation(COMMAND_CONTROLLER_STATE); const [commandedState, setCommandedState] = useState("IDLE"); + const [hobbsTime, setHobbsTime] = useState(props.instance.hobbsTime); + async function handleSave() { setAllowEdit(false); + await updateInstanceHobbsTime({ variables: { id: props.instance.id, hobbsTime: hobbsTime } }) await updateInstance({ variables: { id: props.instance.id, name: name, status: status } }) await updatePairing({ variables: { id: Number(props.instance.id), accessControllerID: pairedController?.id } }) } @@ -91,6 +99,20 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) { />; } + function formatSeconds(allSeconds: number){ + const hours = Math.floor(allSeconds / 3600); + if (hours > 0){ + return (allSeconds / 3600).toFixed(2) + " hrs" + } + const minutes = Math.floor((allSeconds % 3600) / 60); + const seconds = allSeconds % 60; + if (minutes > 0){ + return (minutes + seconds / 50).toFixed(2) + " min" + } else { + return Math.floor(allSeconds) + "sec" + } + } + function activeUserDisplay() { if (!currentAccessController) { return No User; @@ -108,6 +130,15 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) { } + function hobbsTimeSetField(){ + return setHobbsTime(Math.abs(Number(e.target.value)))} + > + + } + return ( @@ -127,6 +158,16 @@ export default function EquipmentInstanceRow(props: EquipmentInstanceRowProps) { : controllerPairingField() } + + + { + !allowEdit + ? formatSeconds(props.instance.hobbsTime) + : hobbsTimeSetField() + } + + + {activeUserDisplay()} diff --git a/client/src/queries/equipmentInstanceQueries.ts b/client/src/queries/equipmentInstanceQueries.ts index d5a9c43b..c382aa6d 100644 --- a/client/src/queries/equipmentInstanceQueries.ts +++ b/client/src/queries/equipmentInstanceQueries.ts @@ -20,6 +20,7 @@ export interface EquipmentInstance { name: string; status: InstanceStatus; accessController: AccessController | undefined; + hobbsTime: number } export const GET_EQUIPMENT_INSTANCES = gql` @@ -32,6 +33,7 @@ export const GET_EQUIPMENT_INSTANCES = gql` } name status + hobbsTime accessController { id channelID @@ -63,6 +65,7 @@ export const GET_EQUIPMENT_INSTANCE_BY_ID = gql` } name status + hobbsTime reader { id name @@ -106,6 +109,22 @@ export const UPDATE_INSTANCE = gql` } name status + hobbsTime + } + } +`; + +export const UPDATE_INSTANCE_HOBBS_TIME = gql` + mutation UpdateInstanceHobbsTime($id: ID!, $hobbsTime: Int!) { + updateInstanceHobbsTime(id: $id, hobbsTime: $hobbsTime) { + id + equipment { + id + name + } + name + status + hobbsTime } } `; diff --git a/server/src/database/knex/migrations/20260715144751_hobbs_time_tracking.ts b/server/src/database/knex/migrations/20260715144751_hobbs_time_tracking.ts new file mode 100644 index 00000000..229f211e --- /dev/null +++ b/server/src/database/knex/migrations/20260715144751_hobbs_time_tracking.ts @@ -0,0 +1,26 @@ +import type { Knex } from "knex"; + + +export async function up(knex: Knex): Promise { + knex.schema.hasTable("EquipmentInstances").then(function (exists) { + if (!exists) return; + + return knex.schema.alterTable("EquipmentInstances", function (t) { + t.integer("hobbsTime").defaultTo(0); + }); + }); + +} + + +export async function down(knex: Knex): Promise { + knex.schema.hasTable("EquipmentInstances").then(function (exists) { + if (!exists) return; + + return knex.schema.alterTable("EquipmentInstances", function (t) { + t.dropColumn("hobbsTime"); + }); + }); + +} + diff --git a/server/src/database/knex/tables.ts b/server/src/database/knex/tables.ts index d2e5692e..b83ccb44 100644 --- a/server/src/database/knex/tables.ts +++ b/server/src/database/knex/tables.ts @@ -81,7 +81,9 @@ export interface EquipmentInstancesRow { /** The state of the equipment, ACTIVE,RETIRED,NEEDS_REPAIRED, etc. */ status: string; /** Optional FK of the card reader associated with this instance */ - accessControllerID: number | null + accessControllerID: number | null; + /** Required number of seconds that this equipment has been run for */ + hobbsTime: number; } export interface HoldRow { @@ -700,6 +702,7 @@ export interface CoreRow { flags: CoreFlags; sealedDeployment: ACSDeployment | undefined; reportedDeployment: ACSDeployment | undefined; + hobbsTime: number } export enum AccessControllerState { diff --git a/server/src/database/models/api/ACSFormats.ts b/server/src/database/models/api/ACSFormats.ts index 37858d9d..1cb57ebc 100644 --- a/server/src/database/models/api/ACSFormats.ts +++ b/server/src/database/models/api/ACSFormats.ts @@ -4,7 +4,8 @@ import { ACSDeployment } from "../ACS/deployment.js"; export interface CoreStatusReport { channels: { channelID: number, - state: AccessControllerState + state: AccessControllerState, + hobbsTime: number, }[]; currentCardTag: string; } @@ -66,7 +67,8 @@ export enum CoreInfoOptions { TIME = "TIME", STATE = "STATE", HMI = "HMI", - FLAGS = "FLAGS" + FLAGS = "FLAGS", + HOBBS_TIME = "HOBBS_TIME" } export interface CoreInfoRequest { @@ -125,6 +127,7 @@ export interface ServerCommand { action?: CoreActions; identifyChannel?: number; flags?: CoreFlags; + hobbsTime?: {channelID: number, hobbsTime: number}[] } export enum CoreRole { @@ -144,13 +147,18 @@ export interface ServerInfoResponse { }[]; hmi?: { role: CoreRole; + deviceName: string, makerspace: string; channels: { channelID: number; pairedEntity: string; }[]; }; - flags?: CoreFlags + flags?: CoreFlags; + hobbsTime?: { + channelID: number; + hobbsTime: number; + }[] } export interface WelcomeRequest { diff --git a/server/src/database/models/api/ACSOrchestrator.ts b/server/src/database/models/api/ACSOrchestrator.ts index 61c57b19..62169fc1 100644 --- a/server/src/database/models/api/ACSOrchestrator.ts +++ b/server/src/database/models/api/ACSOrchestrator.ts @@ -1,5 +1,5 @@ import { ACSController } from "./ACSController.js"; -import { CoreAuthToRequest, CoreConfigReport, CoreInfoOptions, CoreInfoRequest, CoreLogRequest, CoreStateChangeReport, CoreStatusReport, ServerCommand, ServerInfoResponse } from "./ACSFormats.js"; +import { CoreAuthToRequest, CoreConfigReport, CoreInfoOptions, CoreInfoRequest, CoreLogRequest, CoreRole, CoreStateChangeReport, CoreStatusReport, ServerCommand, ServerInfoResponse } from "./ACSFormats.js"; import * as CoreRepo from "../../repositories/Devices/CoreRepository.js"; import * as ACRepo from "../../repositories/Devices/AccessControllerRepository.js"; import * as AuditLogRepo from "../../repositories/AuditLogs/AuditLogRepository.js"; @@ -10,6 +10,8 @@ import * as DeviceRepo from "../../repositories/Devices/DeviceRepository.js"; import { AccessControllerState, DeviceLogSeverity } from "../../knex/tables.js"; import { AccessAttemptReason } from "../devices/accessController.js"; import { Makerspace } from "../makerspaces/makerspace.js"; +import { Core } from "../devices/core.js"; +import { getInstanceByAccessControllerDeviceAndChannel, getInstanceByAccessControllerID, updateInstanceHobbsTime } from "../../repositories/Equipment/EquipmentInstancesRepository.js"; export class ACSOrchestrator { private static coreControllers: Map = new Map(); @@ -24,16 +26,50 @@ export class ACSOrchestrator { public static async handleCoreStatusReport(deviceID: number, statusReport: CoreStatusReport) { try { + console.log("handle: ", statusReport) const core = await CoreRepo.getCoreByDeviceID(deviceID); - if (core === undefined) { return; } + if (core === undefined) { + console.warn("status report from non-core", deviceID) + return; + } + console.log(core) await core.statusUpdate(statusReport.currentCardTag); + const timesToSend = []; + let shouldUpdateTimes = false + console.log("channels", statusReport.channels) for (let i = 0; i < statusReport.channels.length; i++) { - await core.updateControllerState(statusReport.channels[i].channelID, statusReport.channels[i].state); + const channel = statusReport.channels[i]; + await core.updateControllerState(channel.channelID, channel.state); + const inst = await getInstanceByAccessControllerDeviceAndChannel(deviceID, channel.channelID) + timesToSend.push({ channelID: channel.channelID, hobbsTime: inst? Number(inst.hobbsTime) : 0 }) + if (inst == undefined) { + continue; + } + console.log("inst", inst) + + + if (inst.hobbsTime > channel.hobbsTime) { + console.warn("hobbs tme fiasco", inst, channel) + // warning: the device is wrong about how long + DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { msg: "hobbs-time-mismatch", reported: channel.hobbsTime, stored: inst.hobbsTime }) + shouldUpdateTimes = true + } else { + const updated = await updateInstanceHobbsTime(inst.id, channel.hobbsTime) + console.log("update", updated) + } + } + if (shouldUpdateTimes) { + console.log("hobbs tme mismatch: ", timesToSend) + ACSOrchestrator.getDeviceController(deviceID)?.sendCoreCommand(core, { + hobbsTime: timesToSend + }) + } } catch (e) { + console.warn(e) await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-status-error", error: e }); } } @@ -103,7 +139,7 @@ export class ACSOrchestrator { "auth", core?.makerspaceID, { id: user?.id ?? -1, label: user ? `${user.firstName} ${user.lastName}` : "Unknown User" }, - { id: deviceID, label: core ? core.name : "unkown device" } + { id: deviceID, label: core ? core.name : "unknown device" } ); if (core !== undefined) { @@ -142,27 +178,70 @@ export class ACSOrchestrator { } } + + static async getHMIInfo(core: Core) { + const acs = await core.getAccessControllers() + const welcomeFor = await core.getWelcomeMakerspace() + if (acs.length == 0 && welcomeFor == undefined) { + // this thing is unassociated, doesn't make sense to report as welcome reader or equipment + return undefined; + } + const role = welcomeFor != undefined ? CoreRole.WELCOME : CoreRole.EQUIPMENT + const makerspace = await MakerspaceRepo.getMakerspaceByID(welcomeFor?.id ?? core.makerspaceID) + const channels = await Promise.all(acs.map(async (ac) => { + const entity = await getInstanceByAccessControllerID(ac.id) + return { channelID: ac.channelID, pairedEntity: entity?.name ?? "unknown" }; + })); + return { + role: role, + deviceName: core.name, + makerspace: makerspace?.name ?? "unknown", + channels: channels + } + } + public static async handleCoreInfoRequest(deviceID: number, infoRequest: CoreInfoRequest) { try { + const getHobbsTimes = async (core: Core) => { + const acs = await core.getAccessControllers() + if (acs.length == 0) { + console.warn("warn: hobbs time request from device with no controllers ") + return undefined + } + const times = await Promise.all(acs.map(async (ac) => { + const inst = await getInstanceByAccessControllerDeviceAndChannel(acs[0].deviceID, ac.channelID); + return { channelID: ac.channelID, hobbsTime: inst ? Number(inst.hobbsTime) : 0 }; + })) + return times; + }; + + const core = await CoreRepo.getCoreByDeviceID(deviceID); if (core === undefined) { return; } - + console.log("request", infoRequest.fields, core) const response: ServerInfoResponse = { time: infoRequest.fields.includes(CoreInfoOptions.TIME) ? (new Date).getTime() : undefined, + state: infoRequest.fields.includes(CoreInfoOptions.STATE) ? (await core.getAccessControllers()).map((controller) => ({ id: controller.channelID, state: controller.state })) : undefined, - hmi: undefined, + + hmi: infoRequest.fields.includes(CoreInfoOptions.HMI) ? await this.getHMIInfo(core) : undefined, + flags: infoRequest.fields.includes(CoreInfoOptions.FLAGS) ? { lockWhenIdle: core.flags?.lockWhenIdle ?? false, restartWhenUnused: core.flags?.restartWhenUnused ?? false, welcoming: (await core.getWelcomeMakerspace()) !== undefined - } : undefined + } : undefined, + + hobbsTime: infoRequest.fields.includes(CoreInfoOptions.HOBBS_TIME) ? await getHobbsTimes(core) : undefined } + console.log(response) ACSOrchestrator.getDeviceController(core.deviceID)?.sendCoreInfoResponse(core, response); } catch (e) { + console.error(e) await DeviceLogRepo.createDeviceLog(deviceID, DeviceLogSeverity.MEDIUM, { type: "core-info-request-error", error: e }); } } @@ -190,7 +269,7 @@ export class ACSOrchestrator { if (user === undefined) { ACSOrchestrator.getDeviceController(deviceID)?.sendWelcomeResponse(device, { welcomed: false, cardTagID: cardTagID }); await AuditLogRepo.createAuditLog( - `Unkown card {conceal} failed to sign in to {makerspace}`, + `Unknown card {conceal} failed to sign in to {makerspace}`, "welcome", rawSpace.id, { id: 0, label: cardTagID }, @@ -213,8 +292,8 @@ export class ACSOrchestrator { { id: rawSpace.id, label: rawSpace.name } ); - } catch (_e) { - + } catch (e) { + console.warn("ACS: Error handling welcome request: ", e) } } @@ -228,8 +307,8 @@ export class ACSOrchestrator { ACSOrchestrator.coreControllers.get(coreID)?.sendCoreCommand(core, command); } - } catch (_e) { - + } catch (e) { + console.warn("ACS: Error commanding all cores: ", e) } } @@ -243,8 +322,8 @@ export class ACSOrchestrator { ACSOrchestrator.coreControllers.get(coreID)?.sendCoreCommand(core, command); } - } catch (_e) { - + } catch (e) { + console.warn("ACS: Error commanding makerspace cores: ", e) } } } \ No newline at end of file diff --git a/server/src/database/models/api/MQTTACSController.ts b/server/src/database/models/api/MQTTACSController.ts index 95fc7add..e5050b49 100644 --- a/server/src/database/models/api/MQTTACSController.ts +++ b/server/src/database/models/api/MQTTACSController.ts @@ -5,6 +5,7 @@ import { ACSController } from "./ACSController.js"; import { Core } from "../devices/core.js"; import * as DeviceRepo from "../../repositories/Devices/DeviceRepository.js"; import { Device } from "../devices/device.js"; +import { getCoreByDeviceID } from "../../repositories/Devices/CoreRepository.js"; export default class MQTTACSController extends ACSController { @@ -27,13 +28,13 @@ export default class MQTTACSController extends ACSController { }); MQTTACSController.client.on("connect", (_packet) => { - MQTTACSController.client.subscribe("makerspace/+/device/+/status", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/stateChange", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/log", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/authTo/request", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/config/report", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/info/request", { qos: 2 }); - MQTTACSController.client.subscribe("makerspace/+/device/+/welcome/request", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/status", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/stateChange", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/log", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/authTo/request", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/config/report", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/info/request", { qos: 2 }); + MQTTACSController.client.subscribe("makerspace/device/+/welcome/request", { qos: 2 }); }); MQTTACSController.client.on("error", (error) => console.log(`[MQTTACSController] Error: ${error}`)) @@ -60,8 +61,8 @@ export default class MQTTACSController extends ACSController { private static messageDirecter(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { const topicArray = topic.split("/"); - if (topicArray.length < 5) { return; } - switch (topicArray[4]) { + if (topicArray.length < 4) { return; } + switch (topicArray[3]) { case "status": return MQTTACSController.statusHandler(topic, payload, packet); case "stateChange": @@ -77,7 +78,7 @@ export default class MQTTACSController extends ACSController { case "welcome": return MQTTACSController.welcomeRequestHandler(topic, payload, packet); default: - console.log("UNKOWN TOPIC: ", topicArray[4]); + console.log("UNKNOWN TOPIC: ", topicArray[3]); return; } } @@ -85,7 +86,7 @@ export default class MQTTACSController extends ACSController { private static async statusHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); @@ -102,7 +103,7 @@ export default class MQTTACSController extends ACSController { private static async stateChangeHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); @@ -119,7 +120,7 @@ export default class MQTTACSController extends ACSController { private static async logHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); @@ -136,7 +137,7 @@ export default class MQTTACSController extends ACSController { private static async authToRequestHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); @@ -153,7 +154,7 @@ export default class MQTTACSController extends ACSController { private static async configReportHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); @@ -170,14 +171,14 @@ export default class MQTTACSController extends ACSController { private static async infoRequestHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } MQTTACSController.registerDevice(device.id); const infoRequest: CoreInfoRequest = JSON.parse(payload.toString()); // TODO: INPUT VALIDATION - + console.log("info request") ACSOrchestrator.handleCoreInfoRequest(device.id, infoRequest); } catch (e) { console.error(`[MQTTACSController] handleCoreInfoRequest error: ${e}`); @@ -187,15 +188,19 @@ export default class MQTTACSController extends ACSController { private static async welcomeRequestHandler(topic: string, payload: Buffer, packet: mqtt.IPublishPacket) { try { const topicArray = topic.split("/"); - const makerspaceID = Number(topicArray[1]); - const SN = topicArray[3]; + const SN = topicArray[2]; const device = await DeviceRepo.getDeviceBySN(SN); if (device == undefined) { return; } + const core = await getCoreByDeviceID(device.id ) + if (core == undefined) { return; } + const makerspace = await core?.getWelcomeMakerspace() + if (makerspace == undefined) { return; } + MQTTACSController.registerDevice(device.id); const welcomeRequest: WelcomeRequest = JSON.parse(payload.toString()); - ACSOrchestrator.handleWelcomeRequest(makerspaceID, device.id, welcomeRequest.cardTagID); + ACSOrchestrator.handleWelcomeRequest(makerspace.id, device.id, welcomeRequest.cardTagID); } catch (e) { console.error(`[MQTTACSController] welcomeRequestHandler error: ${e}`); } @@ -203,7 +208,7 @@ export default class MQTTACSController extends ACSController { sendCoreAuthToResponse(core: Core, response: ServerAuthToResponse): boolean { try { - MQTTACSController.client.publish(`makerspace/${core.makerspaceID}/device/${core.SN}/authTo/response`, JSON.stringify(response), { qos: 2 }); + MQTTACSController.client.publish(`makerspace/device/${core.SN}/authTo/response`, JSON.stringify(response), { qos: 2 }); } catch (_e) { return false; } @@ -212,7 +217,7 @@ export default class MQTTACSController extends ACSController { sendCoreConfigUpdate(core: Core, update: ServerConfigUpdateRequest): boolean { try { - MQTTACSController.client.publish(`makerspace/${core.makerspaceID}/device/${core.SN}/config/update`, JSON.stringify(update), { qos: 2 }); + MQTTACSController.client.publish(`makerspace/device/${core.SN}/config/update`, JSON.stringify(update), { qos: 2 }); } catch (_e) { return false; } @@ -220,8 +225,9 @@ export default class MQTTACSController extends ACSController { } sendCoreInfoResponse(core: Core, response: ServerInfoResponse): boolean { + console.log("sending info", response) try { - MQTTACSController.client.publish(`makerspace/${core.makerspaceID}/device/${core.SN}/info/response`, JSON.stringify(response), { qos: 2 }); + MQTTACSController.client.publish(`makerspace/device/${core.SN}/info/response`, JSON.stringify(response), { qos: 2 }); } catch (_e) { return false; } @@ -230,7 +236,7 @@ export default class MQTTACSController extends ACSController { sendCoreCommand(core: Core, command: ServerCommand): boolean { try { - MQTTACSController.client.publish(`makerspace/${core.makerspaceID}/device/${core.SN}/command`, JSON.stringify(command), { qos: 2 }); + MQTTACSController.client.publish(`makerspace/device/${core.SN}/command`, JSON.stringify(command), { qos: 2 }); } catch (_e) { return false; } @@ -239,7 +245,7 @@ export default class MQTTACSController extends ACSController { sendWelcomeResponse(device: Device, response: WelcomeResponse): boolean { try { - MQTTACSController.client.publish(`makerspace/${device.makerspaceID}/device/${device.SN}/welcome/response`, JSON.stringify(response), { qos: 2 }); + MQTTACSController.client.publish(`makerspace/device/${device.SN}/welcome/response`, JSON.stringify(response), { qos: 2 }); } catch (_e) { return false; } diff --git a/server/src/database/models/devices/core.ts b/server/src/database/models/devices/core.ts index e195f34d..14b11c60 100644 --- a/server/src/database/models/devices/core.ts +++ b/server/src/database/models/devices/core.ts @@ -94,7 +94,7 @@ export class Core extends Device implements CoreRow { sessionStartTime: this.sessionStartTime, flags: this.flags, sealedDeployment: this.sealedDeployment, - reportedDeployment: this.reportedDeployment + reportedDeployment: this.reportedDeployment, } } diff --git a/server/src/database/repositories/Devices/AccessControllerRepository.ts b/server/src/database/repositories/Devices/AccessControllerRepository.ts index 3e168227..1ed2cb18 100644 --- a/server/src/database/repositories/Devices/AccessControllerRepository.ts +++ b/server/src/database/repositories/Devices/AccessControllerRepository.ts @@ -2,6 +2,7 @@ import { GraphQLError } from "graphql"; import { knex } from "../../knex/index.js"; import { AccessControllerRow, AccessControllerState } from "../../knex/tables.js"; import { AccessController } from "../../models/devices/accessController.js"; +import { getInstanceByAccessControllerID } from "../Equipment/EquipmentInstancesRepository.js"; export async function getAccessControllersByDeviceID(deviceID: number): Promise { const rawRows = await knex("AccessControllers").where("deviceID", "=", deviceID); @@ -39,7 +40,7 @@ export async function updateAccessControllerStateByDeviceAndChannelID(deviceID: await knex("AccessControllers").update({ state: newState }).where({ deviceID: deviceID, channelID: channelID }); } catch (e) { console.log(`Update Controller state failed: ${e}`) - } + } } export async function updateAccessControllerDurationByDeviceAndChannelID(deviceID: number, channelID: number, tempDuration: number): Promise { diff --git a/server/src/database/repositories/Devices/CoreRepository.ts b/server/src/database/repositories/Devices/CoreRepository.ts index 99848410..069a8cac 100644 --- a/server/src/database/repositories/Devices/CoreRepository.ts +++ b/server/src/database/repositories/Devices/CoreRepository.ts @@ -87,7 +87,7 @@ export async function getMakerspaceWelcomeCores(makerspaceID: number): Promise { diff --git a/server/src/database/repositories/Equipment/EquipmentInstancesRepository.ts b/server/src/database/repositories/Equipment/EquipmentInstancesRepository.ts index fc7c9903..31df34f1 100644 --- a/server/src/database/repositories/Equipment/EquipmentInstancesRepository.ts +++ b/server/src/database/repositories/Equipment/EquipmentInstancesRepository.ts @@ -16,7 +16,7 @@ export async function getInstancesByEquipment(equipmentID: number): Promise { + return (await knex("EquipmentInstances").update({ hobbsTime: hobbsTime }).where({ id }).returning("*"))[0]; +} + + + /** * Fetch an EquipmentInstance by its associate reader ID * @param id unique id of reader @@ -42,6 +53,20 @@ export async function getInstanceByAccessControllerID(accessControllerID: number return await knex("EquipmentInstances").where("accessControllerID", "=", accessControllerID).first(); } +/** + * Fetch an EquipmentInstance by its associated reader by its device and channel id + * @param device_id unique id of device + * @param channel_id unique id of channel + * @returns EquipmentInstance or undefined if not exist + */ +export async function getInstanceByAccessControllerDeviceAndChannel(deviceId: number, channelId: number): Promise { + return await knex("EquipmentInstances as ei") + .leftJoin("AccessControllers as ac","accessControllerID", "=", "ac.id") + .where("ac.deviceID", "=", deviceId) + .andWhere("ac.channelID", "=", channelId) + .select("ei.id", "ei.equipmentID", "name", "status", "accessControllerID", "ei.hobbsTime").first(); +} + export async function getReaderByInstanceId(instanceID: number): Promise { // look up by v2 shlug return await knex("Readers").select("Readers.*") diff --git a/server/src/graphql/resolvers/equipmentInstanceResolver.ts b/server/src/graphql/resolvers/equipmentInstanceResolver.ts index 73c51e27..9a2ca064 100644 --- a/server/src/graphql/resolvers/equipmentInstanceResolver.ts +++ b/server/src/graphql/resolvers/equipmentInstanceResolver.ts @@ -5,9 +5,10 @@ import * as ACRepo from "../../database/repositories/Devices/AccessControllerRep import { ApolloContext } from "../../context.js"; import { EquipmentInstancesRow } from "../../database/knex/tables.js"; import { createInstance, deleteInstance, getInstanceByID, getInstancesByEquipment, setInstanceName, setInstanceStatus } from "../../database/repositories/Equipment/EquipmentInstancesRepository.js"; -import { createUnassocaitedAuditLog } from "../../database/repositories/AuditLogs/AuditLogRepository.js"; +import { createAuditLog, createUnassocaitedAuditLog } from "../../database/repositories/AuditLogs/AuditLogRepository.js"; import { GraphQLError } from "graphql"; import { getUsersFullName } from "../../database/repositories/Users/UserRepository.js"; +import { ACSOrchestrator } from "../../database/models/api/ACSOrchestrator.js"; const EquipmentInstanceResolver = { EquipmentInstance: { @@ -129,7 +130,49 @@ const EquipmentInstanceResolver = { return newInstance; }), + /** + * Update the hobbs time of an equipment instance + * @argument instanceID ID of the instance to modify + * @argument hobbsTime seconds of hobbs time to set + */ + updateInstanceHobbsTime: async ( + _parent: any, + args: { + id: number, + hobbsTime: number + }, + { isStaff }: ApolloContext) => + isStaff(async (user) => { + + const instance = await InstanceRepo.getInstanceByID(args.id); + if (!instance) throw new GraphQLError("Instance does not exist"); + + const equipment = await EquipmentRepo.getEquipmentByID(instance.equipmentID); + if (!equipment) throw new GraphQLError("Instance does not have associated Machine"); + + const room = await RoomRepo.getRoomByID(equipment.roomID) + if (!room) throw new GraphQLError("Instance does not have associated Room"); + + if (!instance.accessControllerID) throw new GraphQLError("Instance has no associated access controller") + + const accessController = await ACRepo.getAccessControllerByID(instance.accessControllerID) + if (!accessController) throw new GraphQLError("Instance has no associated access controller") + + + const newInstance = await InstanceRepo.updateInstanceHobbsTime(args.id, args.hobbsTime); + + ACSOrchestrator.handleSendCoreCommand(accessController.deviceID, { + hobbsTime: [{channelID: accessController.channelID, hobbsTime: args.hobbsTime}] + }) + + await createAuditLog(`{user} set hobbs time of instance '${instance?.name}' of equipment {equipment} to ${args.hobbsTime} seconds from ${instance.hobbsTime} seconds`, 'admin', room.makerspaceID ?? undefined, { id: user.id, label: getUsersFullName(user) }, { id: equipment.id, label: equipment.name }); + + return newInstance; + }), + + + /** * Update the status field of an Equipment Instance * @argument id ID of equipment instance to modify * @argument status New Instance status diff --git a/server/src/graphql/schemas/equipmentInstanceSchema.ts b/server/src/graphql/schemas/equipmentInstanceSchema.ts index 08dc7555..1c6fa78f 100644 --- a/server/src/graphql/schemas/equipmentInstanceSchema.ts +++ b/server/src/graphql/schemas/equipmentInstanceSchema.ts @@ -12,6 +12,7 @@ export const EquipmentInstanceTypeDefs = gql` name: String! status: String accessController: AccessController + hobbsTime: Int! } type Reader { id: ID! @@ -36,6 +37,7 @@ export const EquipmentInstanceTypeDefs = gql` setInstanceName(id: ID!, name: String!): EquipmentInstance deleteInstance(id: ID!): Boolean updateInstance(id: ID!, name: String!, status: String!): EquipmentInstance + updateInstanceHobbsTime(id: ID!, hobbsTime: Int!): EquipmentInstance updateInstanceControllerAssignment(id: Int!, accessControllerID: Int): Boolean } ` \ No newline at end of file