2 Commits

Author SHA1 Message Date
7d2f048932 feat(mobile): shadcn like and tailwind added to make things look yummy
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m21s
2026-04-27 21:23:40 -05:00
649ae1ee9f feat(mobile): new route for the ehs launcher 2026-04-27 21:22:59 -05:00
33 changed files with 1922 additions and 325 deletions

View File

@@ -53,4 +53,17 @@ router.get("/apk/latest", (_, res) => {
return res.sendFile(apkPath); return res.sendFile(apkPath);
}); });
router.get("/apk/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
export default router; export default router;

View File

@@ -4,7 +4,7 @@
"slug": "lst-mobile", "slug": "lst-mobile",
"version": "0.11.1-alpha", "version": "0.11.1-alpha",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/icon_white.png",
"scheme": "lstmobile", "scheme": "lstmobile",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"ios": { "ios": {
@@ -12,19 +12,18 @@
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
"backgroundColor": "#E6F4FE", "foregroundImage": "./assets/adaptive-icon-white.png",
"foregroundImage": "./assets/images/android-icon-foreground.png", "backgroundColor": "#ffffff"
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
}, },
"versionCode": 5, "versionCode": 8,
"minSupportedVersionCode": 1, "minSupportedVersionCode": 4,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile"
}, },
"web": { "web": {
"output": "static", "output": "static",
"favicon": "./assets/images/favicon.png" "favicon": "./assets/images/favicon.png",
"bundler": "metro"
}, },
"plugins": [ "plugins": [
"./plugins/withZebraScanner", "./plugins/withZebraScanner",
@@ -34,8 +33,14 @@
{ {
"backgroundColor": "#208AEF", "backgroundColor": "#208AEF",
"android": { "android": {
"image": "./assets/images/splash-icon.png", "resizeMode": "cover",
"imageWidth": 76 "image": "./assets/splash_white.png",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/splash.png",
"backgroundColor": "#000000"
},
"imageWidth": 200
} }
} }
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
lstMobile/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
lstMobile/assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,9 @@
module.exports = (api) => {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

19
lstMobile/components.json Normal file
View File

@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "global.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

58
lstMobile/global.css Normal file
View File

@@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 63%;
--radius: 0.625rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark:root {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 70.9% 59.4%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 300 0% 45%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

View File

@@ -0,0 +1,9 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, {
input: "./global.css",
inlineRem: 16,
});

3
lstMobile/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint", "lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ", "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease ", "build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && cd .. && copy /Y android\\app\\build\\outputs\\apk\\release\\app-release.apk downloads\\mobile\\lst-mobile.apk",
"update": "adb install android/app/build/outputs/apk/release/app-release.apk" "update": "adb install android/app/build/outputs/apk/release/app-release.apk"
}, },
"dependencies": { "dependencies": {
@@ -18,8 +18,13 @@
"@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10", "@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33", "@react-navigation/native": "^7.1.33",
"@rn-primitives/portal": "^1.4.0",
"@rn-primitives/slot": "^1.4.0",
"@tanstack/react-query": "^5.99.0", "@tanstack/react-query": "^5.99.0",
"axios": "^1.15.0", "axios": "^1.15.0",
"babel-preset-expo": "^55.0.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"expo": "~55.0.15", "expo": "~55.0.15",
"expo-application": "~55.0.14", "expo-application": "~55.0.14",
"expo-constants": "~55.0.14", "expo-constants": "~55.0.14",
@@ -35,16 +40,22 @@
"expo-system-ui": "~55.0.15", "expo-system-ui": "~55.0.15",
"expo-web-browser": "~55.0.14", "expo-web-browser": "~55.0.14",
"lucide-react-native": "^1.8.0", "lucide-react-native": "^1.8.0",
"nativewind": "^4.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "^4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0", "react-native-screens": "~4.23.0",
"react-native-tcp-socket": "^6.4.1",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2", "react-native-worklets": "0.7.2",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.3.6", "zod": "^4.3.6",
"zustand": "^5.0.12" "zustand": "^5.0.12"
}, },

View File

@@ -46,7 +46,7 @@ class ZebraScannerModule(
val source: String? = val source: String? =
intent.getStringExtra("com.symbol.datawedge.source") intent.getStringExtra("com.symbol.datawedge.source")
println("LST SCANNER: data=\$barcodeData label=\$labelType source=\$source") println("LST SCANNER: data=$barcodeData label=$labelType source=$source")
if (barcodeData.isNullOrBlank()) { if (barcodeData.isNullOrBlank()) {
println("LST SCANNER: empty barcode") println("LST SCANNER: empty barcode")
@@ -157,6 +157,8 @@ class ZebraScannerModule(
val props = Bundle().apply { val props = Bundle().apply {
putString("scanner_input_enabled", "true") putString("scanner_input_enabled", "true")
putString("scanner_selection", "auto")
putString("trigger_mode", "2") // 2 = HARD trigger only (recommended) wakes scanner up
} }
putBundle("PARAM_LIST", props) putBundle("PARAM_LIST", props)
@@ -187,10 +189,11 @@ class ZebraScannerModule(
putBundle("PARAM_LIST", props) putBundle("PARAM_LIST", props)
} }
putParcelableArrayList( putParcelableArrayList(
"PLUGIN_CONFIG", "PLUGIN_CONFIG",
arrayListOf(barcodeConfig, intentConfig, keystrokeConfig) arrayListOf(barcodeConfig, intentConfig, keystrokeConfig)
) )
} }
sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig) sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig)

View File

@@ -1,19 +1,39 @@
import { Tabs } from "expo-router"; import { Tabs } from "expo-router";
import { Home, Settings } from "lucide-react-native";
import { useAppStore } from "../../hooks/useAppStore"; import { useAppStore } from "../../hooks/useAppStore";
export default function TabsLayout() { export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
headerShown: false, // Hides the header for all screens in this navigator headerShown: false, // Hides the header for all screens in this navigator
}}> }}
<Tabs.Screen name="scanner" options={{ title: "Scan" }} /> >
<Tabs.Screen name="config" options={{ title: "settings" }} /> <Tabs.Screen
<Tabs.Screen name="logs" options={{ title: "Logs", name="scanner"
href: parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs", options={{
}} title: "Scan",
/> tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
</Tabs> }}
); />
<Tabs.Screen
name="config"
options={{
title: "settings",
tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="logs"
options={{
title: "Logs",
href:
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}}
/>
</Tabs>
);
} }

View File

@@ -1,5 +1,7 @@
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { PortalHost } from "@rn-primitives/portal";
export default function RootLayout() { export default function RootLayout() {
return ( return (
@@ -9,6 +11,7 @@ export default function RootLayout() {
<Stack.Screen name="index" /> <Stack.Screen name="index" />
{/* <Stack.Screen name="(tabs)" /> */} {/* <Stack.Screen name="(tabs)" /> */}
</Stack> </Stack>
<PortalHost />
</> </>
); );
} }

View File

@@ -42,17 +42,17 @@ export default function Index() {
); );
await devDelay(1500); await devDelay(1500);
//router.replace("/scanner"); //router.replace("/scanner");
setReady(true) setReady(true);
return; return;
} }
setMessage(<Text>Checking for updates</Text>) setMessage(<Text>Checking for updates</Text>);
await devDelay(1500) await devDelay(1500);
// TODO if theres an update go to update screen message :D // TODO if theres an update go to update screen message :D
setMessage(<Text>Opening LST scan app</Text>); setMessage(<Text>Opening LST scan app</Text>);
await devDelay(3250); await devDelay(3250);
//router.replace("/scanner"); //router.replace("/scanner");
setReady(true) setReady(true);
} catch (error) { } catch (error) {
console.log("Startup error", error); console.log("Startup error", error);
setMessage(<Text>Something went wrong during startup.</Text>); setMessage(<Text>Something went wrong during startup.</Text>);
@@ -62,9 +62,9 @@ export default function Index() {
startup(); startup();
}, [hasHydrated, hasValidSetup, serverPort, router]); }, [hasHydrated, hasValidSetup, serverPort, router]);
if (ready) { if (ready) {
return <Redirect href="/(tabs)/scanner" />; return <Redirect href="/(tabs)/scanner" />;
} }
return ( return (
<View <View
style={{ style={{

View File

@@ -1,31 +1,121 @@
import React from 'react' import { useCallback, useEffect, useState } from "react";
import { View, Text } from 'react-native' import { Text, View } from "react-native";
import { ScannerTestScreen } from './ScannExample' import { useAppStore } from "../hooks/useAppStore";
import { useAppStore } from '../hooks/useAppStore'; import { sendTcpMessage } from "../lib/tcpScan";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
const STX = "\x02";
const ETX = "\x03";
export default function ProdScanner() { export default function ProdScanner() {
const scannerIdFromStore = useAppStore((s) => s.scannerId); const [lastScan, setLastScan] = useState<any>(null);
return ( const [tagScans, setTagScans] = useState<any>([]);
<View> const scannerIdFromStore = useAppStore((s) => s.scannerId);
<View style={{ alignItems: "center", margin: 10 }}> const serverIp = useAppStore((s) => s.serverIp);
<Text style={{ fontSize: 20, fontWeight: "600" }}>SScanner ID: {scannerIdFromStore}</Text> const serverPort = useAppStore((s) => s.serverPort);
</View>
<View
style={{
marginTop: 50,
alignItems: "center",
}}
>
<Text>Relocate</Text>
<Text>0 / 4</Text>
</View>
{/* <View> const handleScan = useCallback(
<Text>List of recent scanned pallets TBA</Text> async (scan: ZebraScanResult) => {
</View> */} const scanned = scan.data;
<ScannerTestScreen />
</View>
let commandToSend = `${STX}${scannerIdFromStore}@${scanned}${ETX}`;
) // if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
if (scan.data.startsWith("000")) {
commandToSend = `${STX}${scannerIdFromStore}@]C1${scanned}${ETX}`;
setTagScans((prev: any) => [
parseInt(scanned.slice(10, -1) || "000", 10).toString(),
...prev,
]);
}
// if we change commands we want to zero out the last scanned labels
if (/^[a-zA-Z]/.test(scan.data)) {
setTagScans([]);
}
const something = await sendTcpMessage(
commandToSend,
serverIp,
parseInt(serverPort || "0", 10),
);
// Later this is where your TCP send goes.
// const response = await sendTcpMessage(tcpMessage);
setLastScan(something.data[0]);
//console.log("TCP response:", something);
},
[scannerIdFromStore, serverIp, serverPort],
);
console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View>
<View>
<View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
Scanner ID: {parseInt(scannerIdFromStore || "0", 10)}
</Text>
</View>
{!lastScan ? (
<View
style={{
marginTop: 10,
alignItems: "center",
}}
>
<Text className="text-xl font-bold">Waiting on scan....</Text>
</View>
) : (
<View
style={{
marginTop: 10,
alignItems: "center",
}}
>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
{lastScan?.action}
</Text>
{lastScan?.type === "error" ? (
<Text style={{ fontSize: 20, fontWeight: "600" }}>
{lastScan?.message}
</Text>
) : (
<View
style={{
marginTop: 15,
alignItems: "center",
}}
>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
{lastScan?.prompt}
</Text>
<Text style={{ fontSize: 20, fontWeight: "600" }}>
{lastScan?.message}
</Text>
</View>
)}
</View>
)}
</View>
<ScannedLabelBox labels={tagScans} />
</View>
);
} }

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Text, View } from "react-native"; import { Button, Text, View } from "react-native";
import { sendTcpMessage } from "../lib/tcpScan";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
const STX = "\x02"; const STX = "\x02";
@@ -13,20 +14,19 @@ export function ScannerTestScreen() {
const scanned = scan.data; const scanned = scan.data;
// Hard-coded command example: let commandToSend = `${STX}98@${scanned}${ETX}`;
// <stx>98@{scanned}<etx>
const tcpMessage = `${STX}98@${scanned}${ETX}`;
console.log("TCP message to send:", tcpMessage); // if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
console.log("TCP message visible:", `<stx>98@${scanned}<etx>`); if (scan.data.startsWith("000")) {
commandToSend = `${STX}98@]C1${scanned}${ETX}`;
}
const something = await sendTcpMessage(commandToSend, "10.44.0.26", 50001);
// Later this is where your TCP send goes. // Later this is where your TCP send goes.
// const response = await sendTcpMessage(tcpMessage); // const response = await sendTcpMessage(tcpMessage);
const fakeResponse = `Would send TCP: <stx>98@${scanned}<etx>`; console.log("TCP response:", something);
setLastResponse(JSON.stringify(something));
console.log("TCP response:", fakeResponse);
setLastResponse(fakeResponse);
}; };
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,50 @@
import { ScrollView, Text, View } from "react-native";
type ScannedLabel = {
id: string;
barcode: string;
createdAt: string;
};
type ScannedLabelBoxProps = {
labels: ScannedLabel[];
};
export function ScannedLabelBox({ labels }: ScannedLabelBoxProps) {
return (
<View style={{ flex: 1, marginTop: 30 }}>
<Text style={{ fontSize: 18, fontWeight: "700", marginBottom: 8 }}>
Current scanned labels
</Text>
<ScrollView
style={{
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 8,
padding: 2,
margin: 2,
}}
contentContainerStyle={{ gap: 2 }}
>
{labels.length === 0 ? (
<Text style={{ color: "#777" }}>No labels scanned yet</Text>
) : (
labels.map((label) => (
<View
key={`${label}`}
style={{
padding: 2,
borderRadius: 8,
backgroundColor: "#f2f2f2",
}}
>
<Text style={{ fontSize: 18, fontWeight: "700" }}>{label}</Text>
</View>
))
)}
</ScrollView>
</View>
);
}

View File

@@ -0,0 +1,52 @@
import { Text, TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { View } from 'react-native';
function Card({ className, ...props }: React.ComponentProps<typeof View>) {
return (
<TextClassContext.Provider value="text-card-foreground">
<View
className={cn(
'bg-card border-border flex flex-col gap-6 rounded-xl border py-6 shadow-sm shadow-black/5',
className
)}
{...props}
/>
</TextClassContext.Provider>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('flex flex-col gap-1.5 px-6', className)} {...props} />;
}
function CardTitle({
className,
...props
}: React.ComponentProps<typeof Text>) {
return (
<Text
role="heading"
aria-level={3}
className={cn('font-semibold leading-none', className)}
{...props}
/>
);
}
function CardDescription({
className,
...props
}: React.ComponentProps<typeof Text>) {
return <Text className={cn('text-muted-foreground text-sm', className)} {...props} />;
}
function CardContent({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('px-6', className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<typeof View>) {
return <View className={cn('flex flex-row items-center px-6', className)} {...props} />;
}
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@@ -0,0 +1,88 @@
import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
const textVariants = cva(
cn(
'text-foreground text-base',
Platform.select({
web: 'select-text',
})
),
{
variants: {
variant: {
default: '',
h1: cn(
'text-center text-4xl font-extrabold tracking-tight',
Platform.select({ web: 'scroll-m-20 text-balance' })
),
h2: cn(
'border-border border-b pb-2 text-3xl font-semibold tracking-tight',
Platform.select({ web: 'scroll-m-20 first:mt-0' })
),
h3: cn('text-2xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
h4: cn('text-xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
p: 'mt-3 leading-7 sm:mt-6',
blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6',
code: cn(
'bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold'
),
lead: 'text-muted-foreground text-xl',
large: 'text-lg font-semibold',
small: 'text-sm font-medium leading-none',
muted: 'text-muted-foreground text-sm',
},
},
defaultVariants: {
variant: 'default',
},
}
);
type TextVariantProps = VariantProps<typeof textVariants>;
type TextVariant = NonNullable<TextVariantProps['variant']>;
const ROLE: Partial<Record<TextVariant, Role>> = {
h1: 'heading',
h2: 'heading',
h3: 'heading',
h4: 'heading',
blockquote: Platform.select({ web: 'blockquote' as Role }),
code: Platform.select({ web: 'code' as Role }),
};
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
h1: '1',
h2: '2',
h3: '3',
h4: '4',
};
const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
asChild = false,
variant = 'default',
...props
}: React.ComponentProps<typeof RNText> &
TextVariantProps & {
asChild?: boolean;
}) {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}
role={variant ? ROLE[variant] : undefined}
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
{...props}
/>
);
}
export { Text, TextClassContext };

View File

@@ -1,7 +1,7 @@
import TcpSocket from "react-native-tcp-socket"; import TcpSocket from "react-native-tcp-socket";
const STX = "\x02"; // const STX = "\x02";
const ETX = "\x03"; // const ETX = "\x03";
type TcpResponse = { type TcpResponse = {
success: boolean; success: boolean;
@@ -9,26 +9,154 @@ type TcpResponse = {
data: string[]; data: string[];
}; };
function parseErpResponse(buffer: Buffer) {
const text = buffer
.toString("utf8")
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|#[0-9A-Za-z])/g, "")
.replace(/\x02/g, "")
.replace(/\x03/g, "")
.trim();
const noHeader = text.replace(/^\d+@/, "");
console.log(text);
if (!noHeader.includes("Scan:")) {
return {
raw: text,
type: "error",
message: noHeader.trim(),
lines: [noHeader.trim()],
};
}
const [actionPart, scanPart = ""] = noHeader.split("Scan:");
const action = actionPart.trim();
const scanClean = scanPart.trim();
const successMatch = scanClean.match(/^(.*?)\s+V$/);
if (successMatch) {
const prompt = successMatch[1].trim();
return {
raw: text,
type: "success",
action,
prompt,
status: "V",
lines: [action, prompt, "V"],
};
}
// // Handles: "Production lotInvalid barcode"
// const knownErrors = [
// "Invalid barcode",
// "Invalid machine",
// "Not on stock",
// "Article tolerance for consolidation not satisfied",
// ].sort((a, b) => b.length - a.length);
// const foundError = knownErrors.find((err) => scanClean.includes(err));
// if (foundError) {
// const prompt = scanClean.replace(foundError, "").trim();
// return {
// raw: text,
// type: "error",
// action,
// prompt,
// message: foundError,
// lines: [action, prompt, foundError].filter(Boolean),
// };
// }
// return {
// raw: text,
// type: "pending",
// action,
// prompt: scanClean,
// lines: [action, scanClean].filter(Boolean),
// };
const unitMatch = scanClean.match(/^(Unit\s+\d+\/\d+)(.*)$/);
if (unitMatch) {
const prompt = unitMatch[1].trim(); // "Unit 1/4"
const remainder = unitMatch[2].trim(); // everything after
// SUCCESS
if (remainder === "V") {
return {
raw: text,
type: "success",
action,
prompt,
status: "V",
lines: [action, prompt, "V"],
};
}
// Known ERP errors
const knownErrors = [
"Invalid barcode",
"Invalid machine",
"Not on stock",
"Article tolerance for consolidation not satisfied",
];
const foundError = knownErrors.find((err) =>
remainder.toLowerCase().includes(err.toLowerCase()),
);
if (foundError) {
return {
raw: text,
type: "error",
action,
prompt,
message: foundError,
lines: [action, prompt, foundError],
};
}
if (remainder) {
return {
raw: text,
type: "prompt",
action,
prompt,
message: remainder,
lines: [action, prompt, remainder],
};
}
return {
raw: text,
type: "pending",
action,
prompt,
lines: [action, prompt],
};
}
}
/** /**
* Sends a Zebra-style TCP message: * Sends a Zebra-style TCP message:
* <STX>98@{scanned}<ETX> * <STX>98@{scanned}<ETX>
*/ */
export async function sendTcpMessage( export async function sendTcpMessage(
scanned: string, command: string,
host: string, host: string,
port: number, port: number,
timeoutMs = 5000, timeoutMs = 5000,
): Promise<TcpResponse> { ): Promise<TcpResponse> {
return new Promise((resolve) => { return new Promise((resolve) => {
const responses: string[] = []; const responses: any = [];
const client = TcpSocket.createConnection({ host, port }, () => { const client = TcpSocket.createConnection({ host, port }, () => {
const payload = `${STX}98@${scanned}${ETX}`; console.log("Sending TCP (visible):", `${command}`);
console.log("Sending TCP (raw):", payload); client.write(command);
console.log("Sending TCP (visible):", `<stx>98@${scanned}<etx>`);
client.write(payload);
}); });
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@@ -42,10 +170,17 @@ export async function sendTcpMessage(
}, timeoutMs); }, timeoutMs);
client.on("data", (data) => { client.on("data", (data) => {
const text = data.toString(); //const text = data.toString();
console.log("TCP received:", text); //console.log("TCP received:", text);
const parsed = parseErpResponse(data);
responses.push(text); responses.push(parsed);
clearTimeout(timeout);
resolve({
success: true,
message: "TCP Response",
data: responses,
});
}); });
client.on("error", (err) => { client.on("error", (err) => {

View File

@@ -0,0 +1,81 @@
import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native";
export const THEME = {
light: {
background: "hsl(0 0% 100%)",
foreground: "hsl(0 0% 3.9%)",
card: "hsl(0 0% 100%)",
cardForeground: "hsl(0 0% 3.9%)",
popover: "hsl(0 0% 100%)",
popoverForeground: "hsl(0 0% 3.9%)",
primary: "hsl(0 0% 9%)",
primaryForeground: "hsl(0 0% 98%)",
secondary: "hsl(0 0% 96.1%)",
secondaryForeground: "hsl(0 0% 9%)",
muted: "hsl(0 0% 96.1%)",
mutedForeground: "hsl(0 0% 45.1%)",
accent: "hsl(0 0% 96.1%)",
accentForeground: "hsl(0 0% 9%)",
destructive: "hsl(0 84.2% 60.2%)",
border: "hsl(0 0% 89.8%)",
input: "hsl(0 0% 89.8%)",
ring: "hsl(0 0% 63%)",
radius: "0.625rem",
chart1: "hsl(12 76% 61%)",
chart2: "hsl(173 58% 39%)",
chart3: "hsl(197 37% 24%)",
chart4: "hsl(43 74% 66%)",
chart5: "hsl(27 87% 67%)",
},
dark: {
background: "hsl(0 0% 3.9%)",
foreground: "hsl(0 0% 98%)",
card: "hsl(0 0% 3.9%)",
cardForeground: "hsl(0 0% 98%)",
popover: "hsl(0 0% 3.9%)",
popoverForeground: "hsl(0 0% 98%)",
primary: "hsl(0 0% 98%)",
primaryForeground: "hsl(0 0% 9%)",
secondary: "hsl(0 0% 14.9%)",
secondaryForeground: "hsl(0 0% 98%)",
muted: "hsl(0 0% 14.9%)",
mutedForeground: "hsl(0 0% 63.9%)",
accent: "hsl(0 0% 14.9%)",
accentForeground: "hsl(0 0% 98%)",
destructive: "hsl(0 70.9% 59.4%)",
border: "hsl(0 0% 14.9%)",
input: "hsl(0 0% 14.9%)",
ring: "hsl(300 0% 45%)",
radius: "0.625rem",
chart1: "hsl(220 70% 50%)",
chart2: "hsl(160 60% 45%)",
chart3: "hsl(30 80% 55%)",
chart4: "hsl(280 65% 60%)",
chart5: "hsl(340 75% 55%)",
},
};
export const NAV_THEME: Record<"light" | "dark", Theme> = {
light: {
...DefaultTheme,
colors: {
background: THEME.light.background,
border: THEME.light.border,
card: THEME.light.card,
notification: THEME.light.destructive,
primary: THEME.light.primary,
text: THEME.light.foreground,
},
},
dark: {
...DarkTheme,
colors: {
background: THEME.dark.background,
border: THEME.dark.border,
card: THEME.dark.card,
notification: THEME.dark.destructive,
primary: THEME.dark.primary,
text: THEME.dark.foreground,
},
},
};

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,76 @@
const { hairlineWidth } = require("nativewind/theme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
borderWidth: {
hairline: hairlineWidth(),
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
future: {
hoverOnlyWhenSupported: true,
},
plugins: [require("tailwindcss-animate")],
};

View File

@@ -4,7 +4,8 @@
"strict": true, "strict": true,
"paths": { "paths": {
"@/*": [ "@/*": [
"./src/*" "./src/*",
"./*"
], ],
"@/assets/*": [ "@/assets/*": [
"./assets/*" "./assets/*"
@@ -15,6 +16,7 @@
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
".expo/types/**/*.ts", ".expo/types/**/*.ts",
"expo-env.d.ts" "expo-env.d.ts",
"nativewind-env.d.ts"
] ]
} }