Skip to content

Commit

Permalink
refactor: add cac
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyaliao committed Oct 7, 2024
1 parent a9ea329 commit a84c5e5
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 246 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ logs
node_modules
temp

data/airaConnect.machineryMessages*.json
airaConnect.machineryMessages*.json
output/*.txt
preprocessed_data.json
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@
"lint": "eslint .",
"release": "bumpp",
"start": "esno src/index.ts",
"preprocess": "esno src/preprocess.ts",
"verify": "esno src/verify.ts",
"report": "esno src/index.ts report",
"test": "vitest",
"typecheck": "tsc --noEmit",
"clean": "find output -type f ! -name 'README.md' -delete"
Expand All @@ -55,6 +54,7 @@
"@antfu/utils": "^0.7.10",
"@types/node": "^22.5.4",
"bumpp": "^9.5.2",
"cac": "^6.7.14",
"consola": "^3.2.3",
"eslint": "^9.9.1",
"esno": "^4.7.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

207 changes: 22 additions & 185 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,193 +1,30 @@
import { execSync as exec } from 'node:child_process'
import fs from 'node:fs/promises'
import process from 'node:process'
import { consola } from 'consola'
import dotenv from 'dotenv'
import { exit, formatDate, performanceUtils, prompt, readFileContent, readFolderNames } from './utils'
import type { serializableData } from './preprocess'
import type { Device, Payload, ProcessedResult, ProcessedSignal } from './type'
import cac from 'cac'
import { generateReportLastUpdate } from '~/modules/reportLastUpdate'
import { generateReportNotWorking } from '~/modules/reportNotWorking'
import { exit } from './utils'

dotenv.config()
const cli = cac()

async function main(): Promise<void> {
consola.start('開始分析資料')
const cost = performanceUtils()

consola.info('檢查檔案中')

const nameMapPrefix = process.env.Name_Map || 'airaConnect.maps'
const nameDevicePrefix = process.env.Name_Device || 'airaConnect.devices'
const nameMessagePrefix = process.env.Name_Message || 'airaConnect.machineryMessages.test'

const files = await readFolderNames()

const isMapFileExist = files.some(file => file.startsWith(nameMapPrefix))
if (!isMapFileExist) {
consola.error('地圖檔案不存在')
exit()
}

const isDeviceFileExist = files.some(file => file.startsWith(nameDevicePrefix))
if (!isDeviceFileExist) {
consola.error('機具檔案不存在')
exit()
}

const filterMessageFiles = files
.filter(file => file.startsWith(nameMessagePrefix!) && file.endsWith('.json'))
.map(file => file.replace('.json', ''))
if (filterMessageFiles.length === 0) {
consola.error('解析檔案不存在')
exit()
}

const selectedFiles = await prompt('請選擇分析的檔案(可多選):', {
type: 'multiselect',
options: filterMessageFiles,
cli
.command('report', 'generate report')
.option('--type <type>', 'Set report type', {
default: 'not-working',
})

let payload: Payload[] = []

consola.info('讀取檔案中')
for (const selectFile of selectedFiles) {
const fileContent = await readFileContent<Payload>(selectFile)
payload = payload.concat(fileContent)
}
consola.info(`共有 ${payload.length} 筆資料,共花費 ${(cost() / 1000).toFixed(2)} 秒`)

let preprocessedData: serializableData
try {
await fs.access('preprocessed_data.json')
}
catch {
consola.info('預處理數據不存在,將重新生成')

exec('nr preprocess', { stdio: 'inherit' })
}
finally {
const data = await fs.readFile('preprocessed_data.json', 'utf-8')
preprocessedData = JSON.parse(data)
}

const { signals: transformedPayloads, startTime, endTime } = processPayload(payload)
payload.length = 0

const { areaMap, devicesMap } = preprocessedData

const resultString = formatOutput(areaMap, devicesMap, transformedPayloads, startTime, endTime)

consola.info(`已處理完畢,正在寫入檔案,共花費 ${(cost() / 1000).toFixed(2)} 秒`)

await writeOutput(resultString)

consola.success('已寫入檔案,共花費')
}

function processPayload(payload: Payload[]): ProcessedResult {
let startTime = Infinity
let endTime = -Infinity

const groupedPayload = payload.reduce((acc, item) => {
const key = `${item.source.gatewayId}/${item.source.communicationEquipmentId}`

if (!acc.has(key)) {
const allDias = new Set<string>()
for (let i = 0; i <= 15; i++) {
allDias.add(`DI${i}`)
}

acc.set(key, {
gatewayId: item.source.gatewayId,
communicationEquipmentId: item.source.communicationEquipmentId,
channel: item.source.channel,
diResults: allDias,
lastUpdateTime: item.timestamp,
})
.action((options) => {
if (options.type === 'not-working') {
generateReportNotWorking()
}

const entry = acc.get(key)!

for (let i = 0; i <= 15; i++) {
const diKey = `DI${i}`

if (item.data[diKey as keyof typeof item.data] === 1) {
entry.diResults.delete(diKey)
}
else if (options.type === 'last-update') {
generateReportLastUpdate()
}

// 更新時間範圍
entry.lastUpdateTime = Math.max(entry.lastUpdateTime, item.timestamp)
startTime = Math.min(startTime, item.timestamp)
endTime = Math.max(endTime, item.timestamp)

return acc
}, new Map<string, Omit<ProcessedSignal, 'diResults'> & { diResults: Set<string> }>())

// 過濾並轉換結果
const signals = Array.from(groupedPayload.values())
.map(({ diResults, ...rest }) => ({
...rest,
diResults: Array.from(diResults),
}))
.filter(signal => signal.diResults.length > 0)

return { signals, startTime, endTime }
}

/**
* 格式化輸出資料
*/
function formatOutput(
areaMap: Record<string, string>,
devicesMap: Record<string, Device>,
processedSignals: ProcessedSignal[],
startTime: number,
endTime: number,
): string {
const startDate = new Date(startTime)
const endDate = new Date(endTime)
const formattedDate = `${startDate.getMonth() + 1}/${startDate.getDate()} - ${endDate.getMonth() + 1}/${endDate.getDate()}`

const header = `日期\t${formattedDate}\nGateway\t通訊設備\tDI\t區域\t名稱\t燈號\t最後更新時間\n`
const lines: string[] = []

processedSignals.forEach((signal) => {
const [gateway, communicationEquipment] = signal.channel.split('/')
const deviceKey = `${signal.gatewayId}/${signal.communicationEquipmentId}`

const matchingDevices = Object.entries(devicesMap)
.filter(([key]) => {
return key.startsWith(deviceKey)
})
.map(([_, device]) => device)

matchingDevices.forEach((device) => {
const areaName = areaMap[device.areaId] || '--'
const formattedLastUpdateTime = formatDate(signal.lastUpdateTime)

signal.diResults.forEach((diKey) => {
const diNumber = diKey.replace('DI', '')
const matchingSignal = device.signal.find(s => s.pin.replace('R', '') === diNumber)

if (matchingSignal) {
lines.push(`${gateway}\t${communicationEquipment}\t${diKey}\t${areaName}\t${device.name}\t${matchingSignal.light}\t${formattedLastUpdateTime}`)
}
})
})
})

return header + lines.join('\n')
}

/**
* 寫入結果到檔案
*/
async function writeOutput(resultString: string): Promise<void> {
const date = new Date()
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
const outputFileName = `../output/${formattedDate}.txt`

await fs.writeFile(new URL(outputFileName, import.meta.url), resultString, 'utf-8')
}
cli
.command('', 'Show help')
.action(() => {
cli.outputHelp()
exit()
})

main()
cli.help()
cli.parse()
Empty file.
5 changes: 5 additions & 0 deletions src/modules/reportLastUpdate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { consola } from 'consola'

export async function generateReportLastUpdate(): Promise<void> {
consola.start('開始產生機具最後更新報表')
}
44 changes: 44 additions & 0 deletions src/modules/reportNotWorking/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export interface Payload {
_id: string
source: {
host: string
port: number
gatewayId: string
communicationEquipmentId: string
channel: string
}
data: {
Time: string
DI0: number
DI1: number
DI2: number
DI3: number
DI4: number
DI5: number
DI6: number
DI7: number
DI8: number
DI9: number
DI10: number
DI11: number
DI12: number
DI13: number
DI14: number
DI15: number
}
timestamp: number
}

export interface ProcessedResult {
signals: ProcessedSignal[]
startTime: number
endTime: number
}

export interface ProcessedSignal {
gatewayId: string
communicationEquipmentId: string
channel: string
diResults: string[]
lastUpdateTime: number
}
Loading

0 comments on commit a84c5e5

Please sign in to comment.