diff --git a/.gitignore b/.gitignore index 606adae..8b44cca 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,5 @@ out/ .idea/ # Data Directory -data/ \ No newline at end of file +data/ +*.test \ No newline at end of file diff --git a/forge.config.ts b/forge.config.ts index 3c8eea0..aee77b1 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -20,7 +20,13 @@ const config: ForgeConfig = { new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), - new MakerDeb({})], + new MakerDeb({ + options: { + depends: [ + "libpcsclite1", "libpcsclite-dev", "pcscd" + ] + } + })], plugins: [ new AutoUnpackNativesPlugin({}), new WebpackPlugin({ diff --git a/package.json b/package.json index bc9e8f5..5085619 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@mui/material": "^5.15.14", "@types/websocket": "^1.0.10", "electron-squirrel-startup": "^1.0.0", + "nfc-pcsc": "^0.8.1", "node-wifi-scanner": "git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/IPCHandler.ts b/src/IPCHandler.ts index bcf23a9..aa50d17 100644 --- a/src/IPCHandler.ts +++ b/src/IPCHandler.ts @@ -1,9 +1,9 @@ import { ipcMain } from 'electron'; import IpcMainInvokeEvent = Electron.IpcMainInvokeEvent; -import {IPCAnswer, IPCChannel, IPCRequest} from "./RawConstants"; +import {IPCAnswer, IPCChannel, IPCListenChannels, IPCRequest} from "./RawConstants"; -export const IPCHandler = ( +export const IPCHandle = ( channel: IPCChannel, listener: ( event: IpcMainInvokeEvent, @@ -14,4 +14,10 @@ export const IPCHandler = ( ipcMain.handle(channel, listener); }; -// \ No newline at end of file +export const IPCSend = ( + channel: IPCListenChannels, + message: IPCAnswer, + ...args: any +): void => { + global.mainWebContents.send(channel, message, ...args); +}; \ No newline at end of file diff --git a/src/NFCHandler.ts b/src/NFCHandler.ts new file mode 100644 index 0000000..739e1fb --- /dev/null +++ b/src/NFCHandler.ts @@ -0,0 +1,235 @@ +import {NFC} from "nfc-pcsc"; +import {IPCSend} from "./IPCHandler"; +import { + IPCListenChannels, + NFCAccountCard, + NFCCard, + NFCCardType, + NFCPropertyCard, + NFCTaskCard, + PropertyColor, TaskType +} from "./RawConstants"; +import * as fs from "fs"; +import path from "path"; + + +export default class NFCHandler { + public static initPCSC() { + const nfc = new NFC(); // optionally you can pass logger + + nfc.on('reader', reader => { + + console.log(`${reader.name} device attached`); + + // enable when you want to auto-process ISO 14443-4 tags (standard=TAG_ISO_14443_4) + // when an ISO 14443-4 is detected, SELECT FILE command with the AID is issued + // the response is available as card.data in the card event + // see examples/basic.js line 17 for more info + // reader.aid = 'F222222222'; + + reader.on('card', card => { + // card is object containing following data + // [always] String type: TAG_ISO_14443_3 (standard nfc tags like MIFARE) or TAG_ISO_14443_4 (Android HCE and others) + // [always] String standard: same as type + // [only TAG_ISO_14443_3] String uid: tag uid + // [only TAG_ISO_14443_4] Buffer data: raw data from select APDU response + + console.log(`${reader.name} card detected`, card); + + let parsed = this.formClass(card.uid, card.data.toString("utf8")); + if (parsed && parsed.isComplete) + IPCSend(IPCListenChannels.NFC_CARD, {status: true, data: parsed.card}); + + IPCSend(IPCListenChannels.NFC_RAW, {status: !!parsed, data: parsed}); + }); + + reader.on('card.off', card => { + console.log(`${reader.name} card removed`, card); + }); + + reader.on('error', err => { + console.log(`${reader.name} an error occurred`, err); + }); + + reader.on('end', () => { + console.log(`${reader.name} device removed`); + }); + + }); + + nfc.on('error', err => { + console.log('an error occurred', err); + }); + } + + public static initTest() { + fs.watchFile(path.resolve(process.cwd(), "nfc.test"), (curr, prev) => { + let contents = fs.readFileSync(path.resolve(process.cwd(), "nfc.test")).toString("utf-8"); + let parsed = this.formClass("xyz", contents); + if (parsed) + IPCSend(IPCListenChannels.NFC_CARD, {status: true, data: parsed}); + + IPCSend(IPCListenChannels.NFC_RAW, {status: !!parsed, data: parsed}); + + console.log("Got NFC", parsed); + }) + } + + + /** + * Returns formed nfc card or null if nothing can be parsed + * @param uid + * @param raw + */ + public static formClass(uid: string, raw: string): formClassReturn | null { + /* + NFC-Card FULL + + CARD_TYPE|PROPERTIES + + - ACCOUNT + - PROPERTY + - TASK + */ + let preParsed: NFCCard = { + uid: uid, + raw: raw, + cardType: NFCCardType.INVALID, + }; + + try { + let parsing = raw.split("|"); + switch (parsing[0]) { + case "PROPERTY": { + preParsed.cardType = NFCCardType.PROPERTY; + const splitProperty = parsing[1].split(";"); + /* + 0 Name + 1 Color + 2 Fullset Amount + 3 Buy Value + 4 Mortgage Value + 5 Rent Object (split with ,) + 0 Basic rent + [1 Rent with 1 house + 2 Rent with 2 houses + 3 Rent with 3 houses + 4 Rent with 4 houses + 5 Rent with 1 hotel] + 6 Special Props + "" + "TRAINSTATION" + "UTILITY" + */ + let name = this.valueOr(splitProperty, 0, ""); + let color = this.valueOr(splitProperty, 1, PropertyColor.WHITE); + let fullSetAmount = this.valueOr(splitProperty, 2, 0); + let buyValue = this.valueOr(splitProperty, 3, 0); + let mortgageValue = this.valueOr(splitProperty, 4, 0); + + let rent = 0; + let rent1 = 0; + let rent2 = 0; + let rent3 = 0; + let rent4 = 0; + let rentHotel = 0; + if (splitProperty[5]) { + let rentSplit = splitProperty[5].split(","); + rent = this.valueOr(rentSplit, 0, 0); + rent1 = this.valueOr(rentSplit, 1, undefined); + rent2 = this.valueOr(rentSplit, 2, undefined); + rent3 = this.valueOr(rentSplit, 3, undefined); + rent4 = this.valueOr(rentSplit, 4, undefined); + rentHotel = this.valueOr(rentSplit, 5, undefined); + } + + let specialProperties = this.valueOr(splitProperty, 6, ""); + + let propertyParsed: NFCPropertyCard = { + ...preParsed, + name: name, + color: color, + fullSetAmount: fullSetAmount, + buyValue: buyValue, + mortgageValue: mortgageValue, + rent: rent, + rent1: rent1, + rent2: rent2, + rent3: rent3, + rent4: rent4, + rentHotel: rentHotel, + specialProperties: specialProperties, + }; + + if (splitProperty.length != 7) + return {card: propertyParsed, isComplete: false}; + + return {card: propertyParsed, isComplete: true}; + } + + case "ACCOUNT": { + preParsed.cardType = NFCCardType.ACCOUNT; + const splitAccount = parsing[1].split(";"); + let symbol = this.valueOr(splitAccount, 0, ""); + let nickname = this.valueOr(splitAccount, 1, ""); + let pin = this.valueOr(splitAccount, 2, 0); + + const splitParsed: NFCAccountCard = { + ...preParsed, + symbol: symbol, + nickname: nickname, + pin: Number.parseInt(pin) + } + + if (splitAccount.length != 3) + return {card: splitParsed, isComplete: false}; + return {card: splitParsed, isComplete: true} + } + + case "TASK": { + preParsed.cardType = NFCCardType.ACCOUNT; + const splitTask = parsing[1].split(";"); + let taskType = this.valueOr(splitTask, 0, ""); + let taskAmount1 = this.valueOr(splitTask, 1, 0); + let taskAmount2 = this.valueOr(splitTask, 2, 0); + + const splitParsed: NFCTaskCard = { + ...preParsed, + type: taskType as TaskType, + amount: Number.parseInt(taskAmount1) || 0, + amount2: Number.parseInt(taskAmount2) || 0 + } + + if (splitTask.length < 1 || splitTask.length > 3) + return {card: splitParsed, isComplete: false}; + + return {card: splitParsed, isComplete: true} + } + + default: { + return {card: preParsed, isComplete: false}; + } + + } + } catch (e) { + return null; + } + + } + + private static valueOr(arr: string[], index: number, def: any) { + try { + if (arr[index]) + return arr[index]; + return def; + } catch (e) { + return def; + } + + } +} + +interface formClassReturn { + card: NFCCard; + isComplete: boolean; +} \ No newline at end of file diff --git a/src/RawConstants.ts b/src/RawConstants.ts index 27ed0c2..be921a7 100644 --- a/src/RawConstants.ts +++ b/src/RawConstants.ts @@ -81,6 +81,7 @@ export interface Config { } export enum NFCCardType { + INVALID = "INVALID", ACCOUNT = "ACCOUNT", PROPERTY = "PROPERTY", TASK = "TASK", @@ -124,6 +125,10 @@ export enum PropertyColor { export type NFCPropertyCardSpecialProperties = "TRAINSTATION" | "UTILITY" +/** + * NFC Property Card + * NAME(str);COLOR(str);FULLSET(num);BUYVAL(num);MORTGAGEVAL(num);RENT,RENT1,RENT2,RENT3,RENT4,RENT_HOTEL,SPECIAl_PROPS + */ export interface NFCPropertyCard extends NFCCard { name: string, color: PropertyColor, @@ -136,7 +141,7 @@ export interface NFCPropertyCard extends NFCCard { rent3?: number, rent4?: number, rentHotel?: number, - specialProperties: NFCPropertyCardSpecialProperties|null, + specialProperties: NFCPropertyCardSpecialProperties|"", } export type TaskType = @@ -147,6 +152,13 @@ export type TaskType = | "PAY_FOR_PROPS" | "ESCAPE_JAIl"; + +/** + * NFC Task Card + * RAW: TYPE;AMOUNT;AMOUNT2 + */ export interface NFCTaskCard extends NFCCard { type: TaskType + amount?: number, + amount2?: number } \ No newline at end of file diff --git a/src/SmartMonopoly.ts b/src/SmartMonopoly.ts index a05c040..6f126df 100644 --- a/src/SmartMonopoly.ts +++ b/src/SmartMonopoly.ts @@ -1,8 +1,9 @@ -import {IPCHandler} from "./IPCHandler"; +import {IPCHandle} from "./IPCHandler"; import {FunctionTest, GameRules} from "./RawConstants"; import OSHandler from "./OSHandler"; import CloudHandler from "./CloudHandler"; import {ConfigHandler} from "./ConfigHandler"; +import NFCHandler from "./NFCHandler"; const wifiScan = require("node-wifi-scanner"); @@ -14,10 +15,11 @@ export default class SmartMonopoly { static run() { this.setupIPCEvents(); OSHandler.enableAllWifis().then().catch(console.error); + NFCHandler.initTest(); } static setupIPCEvents() { - IPCHandler("FUNCTION_TEST", async (e, request, args) => { + IPCHandle("FUNCTION_TEST", async (e, request, args) => { let data: FunctionTest = { hasSudo: await OSHandler.checkForSudo(), hasWPASupplicant: false, @@ -30,7 +32,7 @@ export default class SmartMonopoly { } }); - IPCHandler("WIFI_CONNECT", async (e, request, args) => { + IPCHandle("WIFI_CONNECT", async (e, request, args) => { let data = request.data as { ssid: string, psk: string } try { await OSHandler.addWifi(data.ssid, data.psk); @@ -41,7 +43,7 @@ export default class SmartMonopoly { } }); - IPCHandler("WIFI_SCAN", async (e, request) => { + IPCHandle("WIFI_SCAN", async (e, request) => { try { let networks = await OSHandler.scanWifis(); return {status: true, data: networks}; @@ -50,7 +52,7 @@ export default class SmartMonopoly { } }); - IPCHandler("CLOUD_CONNECT", async (e, request) => { + IPCHandle("CLOUD_CONNECT", async (e, request) => { try { await CloudHandler.connect(); return {status: true} @@ -59,7 +61,7 @@ export default class SmartMonopoly { } }); - IPCHandler("SETTINGS", async(e, request) => { + IPCHandle("SETTINGS", async(e, request) => { if(request.data && request.data["rules"]) { let rules = request.data.rules as GameRules; @@ -69,7 +71,7 @@ export default class SmartMonopoly { return {status: true, data: ConfigHandler.get()}; }); - IPCHandler("PREPARING", async(e, request) => { + IPCHandle("PREPARING", async(e, request) => { const useCloud = !!request.data.useCloud; console.log("Preparing - useCloud: " + useCloud); if(useCloud && !CloudHandler.isConnected()) diff --git a/src/global.ts b/src/global.ts new file mode 100644 index 0000000..296c3ef --- /dev/null +++ b/src/global.ts @@ -0,0 +1 @@ +declare var mainWebContents: Electron.WebContents; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 70e5a49..50982a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,8 @@ const createWindow = (): void => { // Background SmartMonopoly.run(); + + global.mainWebContents = mainWindow.webContents; }; // This method will be called when Electron has finished diff --git a/src/nfc-pcsd.d.ts b/src/nfc-pcsd.d.ts new file mode 100644 index 0000000..29ed0c2 --- /dev/null +++ b/src/nfc-pcsd.d.ts @@ -0,0 +1,68 @@ +declare module 'nfc-pcsc' { + export type ListenerSignature = { + [E in keyof L]: (...args: any[]) => any; + }; + + export type DefaultListener = { + [k: string]: (...args: any[]) => any; + }; + + export class TypedEmitter = DefaultListener> { + static defaultMaxListeners: number; + addListener(event: U, listener: L[U]): this; + prependListener(event: U, listener: L[U]): this; + prependOnceListener(event: U, listener: L[U]): this; + removeListener(event: U, listener: L[U]): this; + removeAllListeners(event?: keyof L): this; + once(event: U, listener: L[U]): this; + on(event: U, listener: L[U]): this; + off(event: U, listener: L[U]): this; + emit(event: U, ...args: Parameters): boolean; + eventNames(): U[]; + listenerCount(type: keyof L): number; + listeners(type: U): L[U][]; + rawListeners(type: U): L[U][]; + getMaxListeners(): number; + setMaxListeners(n: number): this; + } + + type Type = 'TAG_ISO_14443_3' | 'TAG_ISO_14443_4'; + + const KEY_TYPE_A = 0x60; + const KEY_TYPE_B = 0x61; + + interface Card { + type: Type; + standard: Type; + uid?: string; + data?: Buffer; + } + + interface ReaderEmitter { + card: (x: Card) => void; + 'card.off': (x: Card) => void; + error: (x: Error) => void; + end: () => void; + } + + export class Reader extends TypedEmitter { + get name(): string; + + authenticate(blockNumber: number, keyType: number, key: string, obsolete?: boolean): Promise; + + read( + blockNumber: number, + length: number, + blockSize?: number, + packetSize?: number, + readClass?: number + ): Promise; + } + + interface NFCEmitter { + reader: (reader: Reader) => void; + error: (error: Error) => void; + } + + export class NFC extends TypedEmitter {} +} \ No newline at end of file diff --git a/src/web/CardSetup.tsx b/src/web/CardSetup.tsx index 09a494f..8de6a60 100644 --- a/src/web/CardSetup.tsx +++ b/src/web/CardSetup.tsx @@ -2,46 +2,51 @@ import { NFCAccountCard, NFCCard, NFCCardType, - NFCPropertyCard, NFCPropertyCardSpecialProperties, + NFCPropertyCard, + NFCTaskCard, PropertyColor, - TaskType, - WiFiNetwork + TaskType } from "../RawConstants"; import React, {Component} from "react"; import { Backdrop, Box, Button, - Fade, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, + Fade, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Grid, InputLabel, MenuItem, - Modal, Radio, RadioGroup, - Select, SelectChangeEvent, Switch, + Modal, + Radio, + RadioGroup, + Select, TextField, Typography } from "@mui/material"; -import WifiPasswordIcon from "@mui/icons-material/WifiPassword"; -import WifiIcon from "@mui/icons-material/Wifi"; type AccountValues = keyof NFCAccountCard; type PropertyValues = keyof NFCPropertyCard; -type TaskValues = - "TYPE" | "AMOUNT1" | "AMOUNT2"; +type TaskValues = keyof NFCTaskCard; interface CardSetupState { validNFCCard: boolean, NFCCardType: NFCCardType, - accountValues: { [key in AccountValues]?: string }, + accountValues: { [key in AccountValues]?: string | number }, propertyValues: { [key in PropertyValues]?: string | number | boolean }, - taskValues: { [key in TaskValues]?: TaskType | number } + taskValues: { [key in TaskValues]?: string | TaskType | number } } interface CardSetupProps { closeCallback: () => void; card?: NFCCard; + validCard: boolean } export default class CardSetup extends Component { @@ -49,30 +54,42 @@ export default class CardSetup extends Component super(props); let defaultType = NFCCardType.ACCOUNT; - let accountValues: { [key in AccountValues]?: string } = {}; - let propertyValues: { [key in PropertyValues]?: string } = {}; - let taskValues: { [key in TaskValues]?: string } = {}; + let accountValues: { [key in AccountValues]?: string | number } = {}; + let propertyValues: { [key in PropertyValues]?: string | number | boolean } = {}; + let taskValues: { [key in TaskValues]?: string | TaskType | number } = {}; if (props.card) { - defaultType = props.card.cardType; + defaultType = props.card.cardType as NFCCardType; + console.log("Detected card type: " + defaultType); switch (defaultType) { case NFCCardType.ACCOUNT: - let card = props.card as NFCAccountCard; + let accountCard = props.card as NFCAccountCard; accountValues = { - symbol: card.symbol, - nickname: card.nickname, - pin: card.pin, + ...accountCard } + break; + case NFCCardType.PROPERTY: + let propertyCard = props.card as NFCPropertyCard; + propertyValues = { + ...propertyCard + } + break; + case NFCCardType.TASK: + let taskCard = props.card as NFCTaskCard; + taskValues = { + ...taskCard + } + break; } } this.state = { validNFCCard: false, - NFCCardType: NFCCardType.ACCOUNT, + NFCCardType: defaultType, accountValues: accountValues, - propertyValues: {}, - taskValues: {}, + propertyValues: propertyValues, + taskValues: taskValues, } } @@ -110,15 +127,14 @@ export default class CardSetup extends Component } changePropertyState(prop: PropertyValues, ev: any) { - if(prop == "fullSetAmount") { + if (prop == "fullSetAmount") { if (ev.target.value > 4) ev.target.value = 4; - if(ev.target.value < 1) + if (ev.target.value < 1) ev.target.value = 1; } - if(prop == "buyValue" || prop == "mortgageValue" || prop.includes("rent")) - { - if(ev.target.value < 0) + if (prop == "buyValue" || prop == "mortgageValue" || prop.includes("rent")) { + if (ev.target.value < 0) ev.target.value = 0; } @@ -303,10 +319,10 @@ export default class CardSetup extends Component value={this.state.propertyValues["specialProperties"]} onChange={(ev) => this.changePropertyState("specialProperties", ev)} > - } label="Keine" /> - } label="Bahnhof" /> - } label="Versorgungswerk" /> - } label="Gefängnis" /> + } label="Keine"/> + } label="Bahnhof"/> + } label="Versorgungswerk"/> + } label="Gefängnis"/> @@ -372,7 +388,7 @@ export default class CardSetup extends Component Karten-Typ