Observer embryo, Maestro done

This commit is contained in:
STEINNI
2026-06-13 13:47:46 +00:00
parent 932b6e4752
commit 26aefd3fe2
45 changed files with 1889 additions and 143 deletions
+22
View File
@@ -0,0 +1,22 @@
export const construct = (redisCnx) => {
}
export const methods = {
dispatchArenaMessage(msg, chan) {
const maestro = this.config.maestro
if(!maestro || !this.maestroSrv) return(false)
if(this.matchesChan(chan, maestro.lifecycle?.godsReadyChannel ?? 'arena:gods:ready')) {
if(msg.eventType === 'readyToStart') {
this.maestroSrv.onReadyToStart(msg)
return(true)
}
}
if(this.debug) console.log(`[${this.redisId}] Arena message (unhandled):`, msg.eventType, chan)
return(false)
},
}
+5
View File
@@ -0,0 +1,5 @@
export function dispatchMessage(redisCnx, msg, chan) {
if(typeof(redisCnx.dispatchArenaMessage) !== 'function') return
redisCnx.dispatchArenaMessage(msg, chan)
}
+12
View File
@@ -0,0 +1,12 @@
import { methods as arenaMethods, construct as arenaConstruct } from './arenaHandlers.js'
import { dispatchMessage } from './dispatch.js'
export const afterLoginMethods = [
arenaConstruct,
]
export const meshActions = {
...arenaMethods,
}
export { dispatchMessage }
+28
View File
@@ -0,0 +1,28 @@
export function dispatchMessage(redisCnx, msg, chan) {
const maestro = redisCnx.config.maestro
if(!maestro?.maestroActionsChannel) return
const actionsChan = redisCnx.fullChan(maestro.maestroActionsChannel)
if(chan != actionsChan) return
const action = msg.action
if(!action || typeof(action) !== 'string') {
console.warn(`[${redisCnx.redisId}] Ignoring message without action on ${chan}`)
return
}
const handler = redisCnx['action_'+action]
if(typeof(handler) != 'function') {
if(redisCnx.debug) console.warn(`[${redisCnx.redisId}] Unknown action ${action} on ${chan}`)
return
}
const payload = ('payload' in msg) ? msg.payload : null
const reqid = ('reqid' in msg) ? msg.reqid.substr(0, 50) : null
const sender = msg.sender || null
const roles = Array.isArray(msg.roles) ? msg.roles : ['*']
if(redisCnx.debug) console.log(`[${redisCnx.redisId}] Dispatching action ${action} from ${sender}`)
handler.call(redisCnx, action, payload, reqid, sender, roles)
}
+14
View File
@@ -0,0 +1,14 @@
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
import { methods as simulation } from './simulation.js'
import { dispatchMessage } from './dispatch.js'
export const afterLoginMethods = [
utilitiesConstruct,
]
export const meshActions = {
...utilities,
...simulation,
}
export { dispatchMessage }
+138
View File
@@ -0,0 +1,138 @@
import { publishActionReply } from '../../actionsHelper.js'
import { isValidUuid } from '../../simRepository.js'
export const methods = {
/* Event-Rx:
{
"action": "STARTSIMULATION",
"reqid": "6az5e4r6a",
"sender": "<user-uuid>",
"roles": ["*"],
"payload": {
"simulationUuid": "...",
"keyframeId": "...",
"infraId": "..."
}
}
*/
async action_STARTSIMULATION(action, payload, reqid, sender, roles) {
const replyOpts = {
action,
reqid,
sender,
replyChannel: this.config.maestro.maestroActionsReply,
}
if(!this.accessRights.canDo(roles, action)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Unauthorized action !',
} })
return
}
if(!sender || !isValidUuid(sender)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Missing or invalid sender (user UUID)',
} })
return
}
if(!payload?.simulationUuid || !payload?.keyframeId) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Missing simulationUuid or keyframeId',
} })
return
}
try {
const result = await this.maestroSrv.startSimulation(sender, payload)
if(!result.ok) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: result.err,
} })
return
}
publishActionReply(this, { ...replyOpts, reply: {
success: true,
payload: {
simulationId: result.simulationId,
keyframeId: result.keyframeId,
infraId: result.infraId,
agentIds: result.agentIds,
},
} })
} catch(err) {
console.error(`[${this.redisId}] STARTSIMULATION failed:`, err)
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: err.message ?? 'STARTSIMULATION failed',
} })
}
},
/* Event-Rx:
{
"action": "STOPSIMULATION",
"reqid": "6az5e4r6a",
"sender": "<user-uuid>",
"payload": { "simulationUuid": "..." }
}
*/
async action_STOPSIMULATION(action, payload, reqid, sender, roles) {
const replyOpts = {
action,
reqid,
sender,
replyChannel: this.config.maestro.maestroActionsReply,
}
if(!this.accessRights.canDo(roles, action)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Unauthorized action !',
} })
return
}
if(!sender || !isValidUuid(sender)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Missing or invalid sender (user UUID)',
} })
return
}
if(!payload?.simulationUuid) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Missing simulationUuid',
} })
return
}
try {
const result = await this.maestroSrv.stopSimulation(sender, payload)
if(!result.ok) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: result.err,
} })
return
}
publishActionReply(this, { ...replyOpts, reply: {
success: true,
payload: { simulationId: result.simulationId },
} })
} catch(err) {
console.error(`[${this.redisId}] STOPSIMULATION failed:`, err)
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: err.message ?? 'STOPSIMULATION failed',
} })
}
},
}
+48
View File
@@ -0,0 +1,48 @@
import { publishActionReply } from '../../actionsHelper.js'
export const construct = (redisCnx) => {
}
export const methods = {
async action_RELOADCONFIG(action, payload, reqid, sender, roles) {
const replyOpts = {
action,
reqid,
sender,
replyChannel: this.config.maestro.maestroActionsReply,
}
if(!this.accessRights.canDo(roles, action)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Unauthorized action !',
} })
return
}
this.reloadAccessRights()
publishActionReply(this, { ...replyOpts, reply: {
success: true,
} })
},
async action_GETCONFIG(action, payload, reqid, sender, roles) {
const replyOpts = {
action,
reqid,
sender,
replyChannel: this.config.maestro.maestroActionsReply,
}
if(!this.accessRights.canDo(roles, action)) {
publishActionReply(this, { ...replyOpts, reply: {
success: false,
err: 'Unauthorized action !',
} })
return
}
publishActionReply(this, { ...replyOpts, reply: {
success: true,
payload: this.getAccessRights(),
} })
},
}
+16
View File
@@ -0,0 +1,16 @@
export function publishActionReply(redisCnx, options) {
const {
action,
reqid,
sender,
reply,
replyChannel,
senderId = 'maestro',
} = options
reply.action = action
reply.sender = senderId
if(reqid) reply.reqid = reqid
const chan = replyChannel.replace(/\[UID\]/g, sender)
redisCnx.redisPublish(chan, reply)
}
+33
View File
@@ -0,0 +1,33 @@
export class ArenaGroom {
constructor(arenaCnx, storage, debug = false) {
this.cnx = arenaCnx
this.storage = storage
this.debug = debug
}
agentHashKey(agentId) {
return(this.storage.agentHashKey.replace(/\[UID\]/g, agentId))
}
async clearArena() {
const ids = await this.cnx.redisSmembers(this.storage.agentsIndexKey)
for(const id of ids) {
await this.cnx.redisDel(this.agentHashKey(id))
}
await this.cnx.redisDel(this.storage.agentsIndexKey)
if(this.debug) console.log(`[Maestro] Cleared arena store (${ids.length} agent(s))`)
}
async seedAgents(agents) {
for(const agent of agents) {
const key = this.agentHashKey(agent.id)
await this.cnx.redisHset(key, 'position', agent.position)
await this.cnx.redisHset(key, 'vector', agent.vector)
await this.cnx.redisSadd(this.storage.agentsIndexKey, agent.id)
}
if(this.debug) console.log(`[Maestro] Groomed ${agents.length} agent(s) into arena store`)
}
}
+264
View File
@@ -0,0 +1,264 @@
import { AccesRights } from '../accesRights.js'
import { createMysqlPool } from '../mysqlClient.js'
import { SimRepository } from './simRepository.js'
import { ArenaGroom } from './arenaGroom.js'
import { MaestroState } from './orchestrationState.js'
export class maestroServer {
constructor(configHelper, allRediscnx, debug) {
this.configHelper = configHelper
this.maestroConfig = configHelper.config
this.allRediscnx = allRediscnx
this.debug = debug
this.accessRights = new AccesRights(this.maestroConfig, debug)
this.arenaCnx = null
this.arenaCnxs = []
this.systemCnx = null
this.db = null
this.simRepo = null
this.arenaGroom = null
this.orchestrationState = MaestroState.IDLE
this.simulationId = null
this.agentIds = []
this.readyGods = new Map()
this.readyQuorumResolve = null
this.readyQuorumTimer = null
}
getMaestroSettings() {
const maestro = this.maestroConfig.maestro ?? {}
return({
senderId: maestro.senderId ?? 'maestro',
lifecycle: {
arenaChannel: maestro.lifecycle?.arenaChannel ?? 'arena:lifecycle',
godsReadyChannel: maestro.lifecycle?.godsReadyChannel ?? 'arena:gods:ready',
},
expectedGods: maestro.expectedGods ?? ['gps'],
readyTimeoutMs: maestro.readyTimeoutMs ?? 30000,
})
}
getArenaStorageSettings() {
const gps = this.maestroConfig.gps ?? {}
return({
agentHashKey: gps.arenaStorage?.agentHashKey ?? 'arena:agents:[UID]',
agentsIndexKey: gps.arenaStorage?.agentsIndexKey ?? 'arena:agents',
})
}
isIdle() {
return(this.orchestrationState === MaestroState.IDLE)
}
isLive() {
return(this.orchestrationState === MaestroState.LIVE)
}
async init() {
const mysqlCfg = this.maestroConfig.mysql
if(!mysqlCfg) {
console.error('[Maestro] Missing mysql config')
return(false)
}
this.db = await createMysqlPool(mysqlCfg)
this.simRepo = new SimRepository(this.db, this.debug)
if(this.debug) console.log('[Maestro] MySQL pool ready')
return(true)
}
refreshArenaGroom() {
if(this.arenaCnx) {
this.arenaGroom = new ArenaGroom(
this.arenaCnx,
this.getArenaStorageSettings(),
this.debug
)
}
}
wireSystemConnexion(cnx) {
cnx.maestroSrv = this
cnx.accessRights = this.accessRights
cnx.reloadAccessRights = () => this.reloadAccessRights()
cnx.getAccessRights = () => this.getAccessRights()
if(!this.systemCnx || cnx.redisConfig.role === 'primary') {
this.systemCnx = cnx
}
}
wireArenaConnexion(cnx) {
cnx.maestroSrv = this
this.arenaCnxs.push(cnx)
if(!this.arenaCnx || cnx.redisConfig.role === 'primary') {
this.arenaCnx = cnx
this.refreshArenaGroom()
}
}
resetOrchestration() {
this.orchestrationState = MaestroState.IDLE
this.simulationId = null
this.agentIds = []
this.readyGods.clear()
this.clearReadyQuorumWait()
}
clearReadyQuorumWait() {
if(this.readyQuorumTimer) {
clearTimeout(this.readyQuorumTimer)
this.readyQuorumTimer = null
}
this.readyQuorumResolve = null
}
completeReadyQuorum(result) {
const resolve = this.readyQuorumResolve
this.clearReadyQuorumWait()
if(typeof(resolve) === 'function') resolve(result)
}
waitForReadyQuorum() {
const { readyTimeoutMs } = this.getMaestroSettings()
return(new Promise(resolve => {
this.readyQuorumResolve = resolve
this.readyQuorumTimer = setTimeout(() => {
this.completeReadyQuorum({
ok: false,
err: `Timeout waiting for readyToStart (${readyTimeoutMs}ms)`,
})
}, readyTimeoutMs)
}))
}
onReadyToStart(msg) {
if(this.orchestrationState !== MaestroState.PREPARING) return
const payload = msg.payload ?? {}
if(payload.simulationId !== this.simulationId) return
const sender = msg.sender
const { expectedGods } = this.getMaestroSettings()
if(!expectedGods.includes(sender)) {
if(this.debug) console.warn(`[Maestro] Ignoring readyToStart from unexpected sender: ${sender}`)
return
}
if(!payload.success) {
this.completeReadyQuorum({
ok: false,
err: payload.err ?? `Participant ${sender} failed prepare`,
})
return
}
this.readyGods.set(sender, payload)
if(this.debug) console.log(`[Maestro] readyToStart from ${sender} (${this.readyGods.size}/${expectedGods.length})`)
for(const god of expectedGods) {
if(!this.readyGods.has(god)) return
}
this.completeReadyQuorum({ ok: true })
}
async publishLifecycle(eventType, payload) {
if(!this.arenaCnx) throw new Error('No arena Redis connection')
const { arenaChannel, senderId } = this.getMaestroSettings().lifecycle
await this.arenaCnx.redisPublish(arenaChannel, {
eventType,
sender: senderId,
payload,
})
if(this.debug) console.log(`[Maestro] Published ${eventType} simulationId=${payload.simulationId}`)
}
async startSimulation(userUuid, payload) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' })
if(!this.isIdle()) return({ ok: false, err: 'A simulation is already in progress' })
const simulationUuid = payload?.simulationUuid
const keyframeId = payload?.keyframeId
const infraId = payload?.infraId ?? null
const access = await this.simRepo.validateSimulationAccess(userUuid, simulationUuid, keyframeId)
if(!access.ok) return(access)
const agentsResult = await this.simRepo.loadKeyframeAgents(keyframeId)
if(!agentsResult.ok) return(agentsResult)
await this.arenaGroom.clearArena()
await this.arenaGroom.seedAgents(agentsResult.agents)
this.simulationId = simulationUuid
this.agentIds = agentsResult.agents.map(a => a.id)
this.readyGods.clear()
this.orchestrationState = MaestroState.PREPARING
const lifecyclePayload = {
simulationId: this.simulationId,
agentIds: this.agentIds,
keyframeId,
infraId,
}
const readyWait = this.waitForReadyQuorum()
await this.publishLifecycle('onYourMarks', lifecyclePayload)
const quorum = await readyWait
if(!quorum.ok) {
await this.arenaGroom.clearArena()
this.resetOrchestration()
return(quorum)
}
await this.publishLifecycle('bigBang', {
simulationId: this.simulationId,
agentIds: this.agentIds,
})
this.orchestrationState = MaestroState.LIVE
if(this.debug) console.log(`[Maestro] LIVE simulationId=${this.simulationId}`)
return({
ok: true,
simulationId: this.simulationId,
keyframeId,
infraId,
agentIds: this.agentIds,
})
}
async stopSimulation(userUuid, payload) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' })
const simulationUuid = payload?.simulationUuid
const access = await this.simRepo.validateSimulationOwner(userUuid, simulationUuid)
if(!access.ok) return(access)
if(this.simulationId && this.simulationId !== simulationUuid) {
return({ ok: false, err: 'Another simulation is active' })
}
await this.arenaGroom.clearArena()
this.resetOrchestration()
if(this.debug) console.log(`[Maestro] Stopped simulationId=${simulationUuid}`)
return({ ok: true, simulationId: simulationUuid })
}
async reloadAccessRights() {
await this.configHelper.refreshAccessRights()
this.maestroConfig.accessRights = this.configHelper.config.accessRights
this.accessRights.refreshAccessRights(this.maestroConfig)
}
getAccessRights() {
return(this.maestroConfig.accessRights)
}
}
+5
View File
@@ -0,0 +1,5 @@
export const MaestroState = {
IDLE: 'idle',
PREPARING: 'preparing',
LIVE: 'live',
}
+136
View File
@@ -0,0 +1,136 @@
import yargs from 'yargs/yargs'
import { hideBin } from 'yargs/helpers'
import 'node:process'
import { RedisConnexion } from '../redisConnexion.js'
import { configHelper } from '../configHelper.js'
import { maestroServer } from './maestroServer.js'
import * as systemMesh from './actions/system/index.js'
import * as arenaMesh from './actions/arena/index.js'
const meshModules = {
system: systemMesh,
arena: arenaMesh,
}
const originalLog = console.log
const originalWarn = console.warn
const originalError = console.error
function logWithTimestamp(originalFn, level, ...args) {
const timestamp = new Date().toISOString()
originalFn(`[${timestamp}] [${level}]`, ...args)
}
console.log = (...args) => logWithTimestamp(originalLog, 'LOG', ...args)
console.warn = (...args) => logWithTimestamp(originalWarn, 'WARN', ...args)
console.error = (...args) => logWithTimestamp(originalError, 'ERROR', ...args)
const argv = yargs(hideBin(process.argv)).command('SimMaestro', 'Simulation orchestrator for P42', {})
.options({
'debug': {
description: 'shows debug info',
alias: 'd',
defaut: false,
type: 'boolean'
},
'config': {
description: 'Points to config file (default: ../config.json)',
alias: 'c',
default: '../config.json',
type: 'string'
},
}).help().version('1.0').argv
const debug = Boolean(process.env.DEBUG) || argv.debug
let cfgh = new configHelper({
localfile: argv.config,
})
function meshRedisConns(mesh, meshName, debug, rootConfig) {
const { redis, ...meshConfig } = mesh
return redis.map(cfg =>
new RedisConnexion({
debug,
config: { ...cfg, ...meshConfig, maestro: rootConfig.maestro },
redisId: cfg.redisId,
meshName,
meshModule: meshModules[meshName],
})
)
}
async function startAllRedis(rootConfig, cfgh) {
if(debug) console.log('Starting all Redis instances...')
const redisConns = [
...meshRedisConns(rootConfig.systemMesh, 'system', debug, rootConfig),
...meshRedisConns(rootConfig.arenaMesh, 'arena', debug, rootConfig),
]
const srv = new maestroServer(cfgh, redisConns, debug)
for(const cnx of redisConns) {
if(cnx.meshName === 'system') srv.wireSystemConnexion(cnx)
else if(cnx.meshName === 'arena') srv.wireArenaConnexion(cnx)
}
const loginResults = await Promise.allSettled(
redisConns.map(async cnx => {
await cnx.redisLogin()
return(cnx.redisId)
})
)
const failedLogin = loginResults.filter(r => r.status !== 'fulfilled')
if(failedLogin.length > 0) {
console.error('Redis login failures:')
failedLogin.forEach((r, i) => {
const id = redisConns[i].redisId
console.error(`login failed for redis:[${id}] → ${r.reason}`)
})
throw new Error(
`Redis login failed for ${failedLogin.length}/${redisConns.length} instances`
)
}
if(debug) console.log('All Redis logins OK')
const chanResults = await Promise.allSettled(
redisConns.map(async cnx => {
await cnx.redisChansStart()
return(cnx.redisId)
})
)
const failedChans = chanResults.filter(r => r.status !== 'fulfilled')
if(failedChans.length > 0) {
console.error('Redis chansStart failures:')
failedChans.forEach((r, i) => {
const id = redisConns[i].redisId
console.error(`chansStart failed for redis:[${id}] → ${r.reason}`)
})
throw new Error(
`Redis chansStart failed for ${failedChans.length}/${redisConns.length} instances`
)
}
if(debug) console.log('All Redis chansStart OK')
return({ redisConns, srv })
}
cfgh.fetchConfig().then(async rootConfig => {
if(!rootConfig) {
console.error('Cannot get a valid configuration ! Aaarrghhh...')
process.exit()
}
console.log(`Debug mode : ${debug ? 'ON' : 'OFF'}`)
const { srv } = await startAllRedis(rootConfig, cfgh)
const dbOk = await srv.init()
if(!dbOk) {
console.error('Maestro MySQL init failed — exiting')
process.exit(1)
}
})
+9
View File
@@ -0,0 +1,9 @@
{
"name": "p42SimMaestro",
"version": "1.0.0",
"description": "Simulation orchestrator God-daemon for P42",
"type": "module",
"dependencies": {
"yargs": "^17.7.2"
}
}
+116
View File
@@ -0,0 +1,116 @@
import { mysqlExecute } from '../mysqlClient.js'
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
export function isValidUuid(val) {
return(typeof(val) === 'string' && UUID_RE.test(val))
}
function parseGpsValues(raw) {
let v = raw
if(v == null) return(null)
if(typeof(v) === 'string') {
try { v = JSON.parse(v) }
catch { return(null) }
}
if(typeof(v) !== 'object') return(null)
const position = v.position
const speed = v.speed ?? v.vector
if(!position || !speed) return(null)
const axes = ['x', 'y', 'z']
for(const axis of axes) {
if(typeof(position[axis]) !== 'number' || typeof(speed[axis]) !== 'number') return(null)
}
return({
position: { x: position.x, y: position.y, z: position.z },
vector: { x: speed.x, y: speed.y, z: speed.z },
})
}
export class SimRepository {
constructor(dbPool, debug = false) {
this.db = dbPool
this.debug = debug
}
async validateSimulationAccess(userUuid, simulationUuid, keyframeId) {
if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' })
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
if(!isValidUuid(keyframeId)) return({ ok: false, err: 'Invalid keyframe ID' })
const rows = await mysqlExecute(this.db, `
SELECT s.sim_id,
BIN_TO_UUID(s.sim_uuid) AS sim_uuid,
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid
FROM p42SIM.simulations s
INNER JOIN p42GUI.simowners o ON o.own_sim_uuid = s.sim_uuid
INNER JOIN p42GUI.users u ON o.own_usr_id = u.usr_id
WHERE u.usr_uuid = ?
AND s.sim_uuid = UUID_TO_BIN(?)
`, [userUuid, simulationUuid])
if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' })
const sim = rows[0]
if(sim.sim_root_kf_uuid !== keyframeId) {
return({ ok: false, err: 'Keyframe does not match simulation root keyframe' })
}
const kfRows = await mysqlExecute(this.db, `
SELECT ekf_uuid
FROM p42SIM.edited_keyframes
WHERE ekf_uuid = UUID_TO_BIN(?)
`, [keyframeId])
if(!kfRows.length) return({ ok: false, err: 'Keyframe not found' })
return({ ok: true, sim })
}
async validateSimulationOwner(userUuid, simulationUuid) {
if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' })
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
const rows = await mysqlExecute(this.db, `
SELECT s.sim_id,
BIN_TO_UUID(s.sim_uuid) AS sim_uuid,
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid
FROM p42SIM.simulations s
INNER JOIN p42GUI.simowners o ON o.own_sim_uuid = s.sim_uuid
INNER JOIN p42GUI.users u ON o.own_usr_id = u.usr_id
WHERE u.usr_uuid = ?
AND s.sim_uuid = UUID_TO_BIN(?)
`, [userUuid, simulationUuid])
if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' })
return({ ok: true, sim: rows[0] })
}
async loadKeyframeAgents(keyframeId) {
const rows = await mysqlExecute(this.db, `
SELECT BIN_TO_UUID(ekfs_agent_id) AS agent_id, ekfs_gps_values
FROM p42SIM.edited_kf_store
WHERE ekfs_ekf_uuid = UUID_TO_BIN(?)
`, [keyframeId])
const agents = []
const errors = []
for(const row of rows) {
const parsed = parseGpsValues(row.ekfs_gps_values)
if(!parsed) {
errors.push(`Invalid GPS values for agent ${row.agent_id}`)
continue
}
agents.push({
id: row.agent_id,
position: parsed.position,
vector: parsed.vector,
})
}
if(errors.length) return({ ok: false, err: errors.join('; '), agents: [] })
return({ ok: true, agents })
}
}
+31
View File
@@ -0,0 +1,31 @@
#!/bin/sh
. /etc/p42/secrets.env
daemon=p42SimMaestro
logfile=maestro.log
pid=$(pgrep -f "$daemon")
if [ -z "$pid" ]
then
node "${daemon}.js" --debug > "$logfile" 2>&1 &
pid=$!
sleep 1
if kill -0 "$pid" 2>/dev/null
then
echo ""
echo "$daemon is now running with PID=$pid"
echo ""
else
echo ""
echo "Failed to start $daemon. Check maestro.log"
echo ""
fi
else
echo ""
echo "$daemon is already running with PID=$pid"
echo ""
fi
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
pid=`ps -ef | grep p42SimMaestro.js |grep -v grep | awk '{print $2}'`
if [ -n "$pid" ]
then
echo "killing pid: $pid"
kill -9 $pid
fi