From 12caccd2ca35f16ed7b66f2594f7f05ef2e1b1ab Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 22 Mar 2025 21:50:56 +0800 Subject: [PATCH] finish database, udp pool --- .gitignore | 3 + bun.lock | 6 ++ flake.nix | 6 ++ package.json | 4 +- server/common.ts | 11 +++ server/database.ts | 128 ++++++++++++++++++++++++++- server/equipment.ts | 159 ++++++++++++++++++++++++++++++++++ server/router.ts | 4 + server/test.ts | 5 ++ server/udp.ts | 58 +++++++++++++ src/components/UploadCard.vue | 16 +++- 11 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 server/common.ts create mode 100644 server/equipment.ts create mode 100644 server/test.ts create mode 100644 server/udp.ts diff --git a/.gitignore b/.gitignore index d7df1b5..0c7f6fd 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ coverage *.sw? *.tsbuildinfo + +# Generated Files +*.sqlite diff --git a/bun.lock b/bun.lock index 0622e17..11dfe4b 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,14 @@ "dependencies": { "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", + "@types/lodash": "^4.17.16", "log-symbols": "^7.0.0", "pinia": "^3.0.1", "trpc-bun-adapter": "^1.2.2", "ts-log": "^2.2.7", "vue": "^3.5.13", "vue-router": "4", + "zod": "^3.24.2", }, "devDependencies": { "@tailwindcss/postcss": "^4.0.12", @@ -243,6 +245,8 @@ "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + "@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="], + "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], @@ -557,6 +561,8 @@ "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "@vue/devtools-core/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/flake.nix b/flake.nix index 96d1929..e8fe474 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,12 @@ default = pkgs.mkShell { packages = with pkgs; [ bun + sqlite + sqls + dbeaver-bin + + # LSP + typescript-language-server ]; shellHook = '' export PATH=$PATH:$HOME/.bun/bin diff --git a/package.json b/package.json index a23c4a2..a2669fd 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "dependencies": { "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", + "@types/lodash": "^4.17.16", "log-symbols": "^7.0.0", "pinia": "^3.0.1", "trpc-bun-adapter": "^1.2.2", "ts-log": "^2.2.7", "vue": "^3.5.13", - "vue-router": "4" + "vue-router": "4", + "zod": "^3.24.2" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.12", diff --git a/server/common.ts b/server/common.ts new file mode 100644 index 0000000..41c94fb --- /dev/null +++ b/server/common.ts @@ -0,0 +1,11 @@ +export function numberToBytes(num: number, bytesLength: number): Uint8Array { + var array = new Uint8Array(bytesLength) + + var i; + for (i = 0; i < bytesLength; i++) { + array[i] = num & (0xFF << (i << 3)) + } + + return array +} + diff --git a/server/database.ts b/server/database.ts index 6883911..3c121df 100644 --- a/server/database.ts +++ b/server/database.ts @@ -1,7 +1,131 @@ import { Database } from "bun:sqlite"; +import { isNumber, isString } from "lodash"; +import { z } from "zod"; const db = new Database("lab.sqlite", { strict: true }) +initDB(); -export function addUser(name: string, password: string) { - const query = db.query() + +const boardSchema = z.object({ + id: z.number(), + name: z.string(), + room: z.string(), + ipv4: z.string().ip({ version: "v4" }), + ipv6: z.string().ip({ version: "v6" }), + port: z.number().nonnegative(), + cmdID: z.number().nonnegative() +}).partial({ + ipv6: true, + cmdID: true +}) + +export type Board = z.infer + +export function isBoard(obj: any): obj is Board { + return boardSchema.safeParse(obj).success } + +const userSchema = z.object({ + id: z.number(), + name: z.string(), + password: z.string(), + boardID: z.number(), +}).partial({ + boardID: true, +}) + +export type User = z.infer + +export function isUser(obj: any): obj is User { + return userSchema.safeParse(obj).success +} + + +function initDB() { + const tables = allTables() + + if (!tables.includes("Users")) { + db.query(` + CREATE TABLE Users( + id INT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + password TEXT NOT NULL + ); + `).run(); + } + + if (!tables.includes("Boards")) + db.query(` + CREATE TABLE Boards( + id INT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + room TEXT NOT NULL, + ipv4 CHAR(16) NOT NULL, + ipv6 CHAR(46) , + port INT NOT NULL + ) + `).run(); +} + +export function allTables(): Array { + const query = db.query(`SELECT name FROM sqlite_master WHERE type='table'`) + var tables = new Array() + // Flaten array + for (const item of query.values()) { + tables.push(item[0]) + } + + query.finalize() + return tables +} + +export function tableColumnName(table: string): Array { + const query = db.query(`PRAGMA table_info(${table})`) + + var columnName = new Array() + for (const column of query.values()) { + columnName.push(column[1]) + } + + return columnName +} + +export function allBoards() { + const query = db.query(`SELECT * FROM Boards`) + const boards = query.all() + + query.finalize() + return boards +} + +export function findBoard(id?: number | string): Board { + let condition: string + if (isNumber(id)) { + condition = `id=${id}` + } else if (isString(id)) { + condition = `name='${id}'` + } else { + throw new Error("Failure: Wrong type when find board") + } + + const query = db.query(`SELECT * FROM Boards WHERE ${condition}`) + + const spRet = boardSchema.safeParse(query.get()) + if (spRet.success) { + return spRet.data + } else { + throw new Error(`Not Found ${id} FPGA Board`) + } +} + +export function findUser(name: string): User { + const query = db.query(`SELECT * FROM Users WHERE name='${name}'`) + + const spRet = userSchema.safeParse(query.get()) + if (spRet.success) { + return spRet.data + } else { + throw new Error(`Failure: Not found ${name} User`) + } +} + diff --git a/server/equipment.ts b/server/equipment.ts new file mode 100644 index 0000000..8e848ee --- /dev/null +++ b/server/equipment.ts @@ -0,0 +1,159 @@ +import type { Board } from "./database"; +import { boardSchema, findBoard } from "./database"; +import { numberToBytes } from "./common"; +import { z } from "zod"; +import _ from "lodash"; +import { udpSocketPool } from "./udp"; + +export namespace EquipmentPackage { + const HEADER_LENGTH = 8 + const BYTES_RETURN_ACK = 0b0001_0000 + const BYTES_TRANSFORM_TYPE = 0b0000_1000 + const BYTES_READ_WRITE_TYPE = 0b0000_0100 + + const HeaderOptionsSchema = z.object({ + returnAck: z.boolean(), + transformType: z.enum(["Fixed", "Extend"]), + readWriteType: z.enum(["r", "w"]) + }).partial() + + export type HeaderOptions = z.infer + + export function isHeaderOptions(obj: any): obj is HeaderOptions { + return HeaderOptionsSchema.safeParse(obj).success + } + + export class Header { + private commandType: number = 0 + private bytesLength: number = 0 + private commmandID: number = 0 + private _reserved: number = 0 + private address: number = 0 + + constructor(options: HeaderOptions) { + this.setCommandType(options) + } + + setCommandType(options: HeaderOptions) { + const validOptions = HeaderOptionsSchema.parse(options) + this.commandType = + (validOptions.returnAck === true ? BYTES_RETURN_ACK : 0) | + (validOptions.transformType === "Extend" ? BYTES_TRANSFORM_TYPE : 0) | + (validOptions.readWriteType === "w" ? BYTES_READ_WRITE_TYPE : 0) + } + + setBytesLength(length: number) { + this.bytesLength = length + } + + setAddress(address: number) { + this.address = address + } + + getCommandType(): HeaderOptions { + return { + returnAck: ((this.commandType & BYTES_RETURN_ACK) === BYTES_RETURN_ACK ? true : false), + transformType: ((this.commandType & BYTES_TRANSFORM_TYPE) === BYTES_TRANSFORM_TYPE ? "Extend" : "Fixed"), + readWriteType: ((this.commandType & BYTES_READ_WRITE_TYPE) === BYTES_READ_WRITE_TYPE ? "w" : "r") + } + } + + incCommandID() { + this.commmandID++ + } + + clearCommandID() { + this.commmandID = 0 + } + + toUint8Array(): Uint8Array { + var array = new Uint8Array(HEADER_LENGTH) + + array[0] = this.commandType + array[1] = this.bytesLength + array[2] = this.commmandID + array[3] = this._reserved + array[4] = this._reserved + + let addressBytes = numberToBytes(this.address, 3) + array[5] = addressBytes[0] + array[6] = addressBytes[1] + array[7] = addressBytes[2] + + return array + } + } + + export class Package { + + private header: Header + private body: Uint8Array + + + constructor(header: Header | HeaderOptions, body?: Uint8Array) { + if (header instanceof Header) { + this.header = header + } else if (isHeaderOptions(header)) { + this.header = new Header(header) + } else { + throw Error("Create EquipmentSocket Failure!") + } + + this.body = (body === undefined) ? new Uint8Array(0) : _.cloneDeep(body) + } + + toUint8Array(): Uint8Array { + const header = this.header.toUint8Array() + const bodyLength = this.body.length + + let total = new Uint8Array(header.length + bodyLength) + total.set(header) + if (bodyLength > 0) + total.set(this.body, header.length) + + return total + } + + } +} + + +export class Equipment { + board: Board + + constructor(name?: string) + constructor(board?: Board) + constructor(arg1: any) { + if (boardSchema.safeParse(arg1).success) { + this.board = arg1 + + } else if (typeof arg1 === "string") { + try { + const board = findBoard(arg1) + this.board = boardSchema.parse(board) + } catch (error) { + throw new Error("Equipment Construction Failure") + } + + } else { + throw new Error("Equipment Construction Failure") + } + } + + send(header: EquipmentPackage.HeaderOptions) { + const pack = new EquipmentPackage.Package(header) + + return udpSocketPool.send(pack.toUint8Array(), this.board.port, this.board.ipv4) + } + + // TODO: add params file + uploadBitStream() { + const header: EquipmentPackage.HeaderOptions = { + returnAck: true, + transformType: "Extend", + readWriteType: "w" + } + + return this.send(header) + } +} diff --git a/server/router.ts b/server/router.ts index df79e85..4c1e36a 100644 --- a/server/router.ts +++ b/server/router.ts @@ -1,8 +1,12 @@ import { router, publicProcedure } from "./trpc.ts" +// import { addUser } from "./database.ts"; export const appRouter = router({ api: router({ status: publicProcedure.query(() => "OK"), + signUp: publicProcedure.query((opts) => { + + }) }) }); diff --git a/server/test.ts b/server/test.ts new file mode 100644 index 0000000..4c65edf --- /dev/null +++ b/server/test.ts @@ -0,0 +1,5 @@ +import { allTables } from "./database"; + + +const tables = allTables(); +console.log(tables); diff --git a/server/udp.ts b/server/udp.ts new file mode 100644 index 0000000..4c6d5f1 --- /dev/null +++ b/server/udp.ts @@ -0,0 +1,58 @@ +import type { udp } from "bun" + +const udpServer = await Bun.udpSocket({ + port: 33000, + socket: { + data(_socket, _buf, _port, _addr) { + // todo : Handle Recieved Data + } + } +}) + +export const udpSocketPool = await createUDPSocketPool(5) + +type bunUDPSocket = udp.Socket<"buffer"> + +export function getUDPServerPort() { + return udpServer.port +} + +export class UDPSocketPool { + freeSockets: Set = new Set() + busySockets: Set = new Set() + + getFreeSocket(): bunUDPSocket { + const socket = this.freeSockets.values().next().value + if (socket !== undefined) { + this.busySockets.add(socket) + this.freeSockets.delete(socket) + } else { + throw Error("Failure: Create udp socket failed") + } + return socket + } + + releaseSocket(socket: any) { + this.freeSockets.add(socket) + this.busySockets.delete(socket) + } + + send(data: udp.Data, port: number, hostname: string) { + const socket = this.getFreeSocket() + socket.send(data, port, hostname) + this.releaseSocket(socket) + } + +} + +export async function createUDPSocketPool(depth: number): Promise { + const pool = new UDPSocketPool() + for (var i = 0; i < depth; i++) { + const socket = await Bun.udpSocket({}) + pool.freeSockets.add(socket) + } + + return pool +} + + diff --git a/src/components/UploadCard.vue b/src/components/UploadCard.vue index 1664f35..1cfe5ce 100644 --- a/src/components/UploadCard.vue +++ b/src/components/UploadCard.vue @@ -23,6 +23,7 @@