Initialize Schnappix Photo Booth application with custom UVC camera, silent Wi-Fi IPP printing, local gallery storage, and kiosk screen pinning support
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import ipp from 'ipp-encoder';
|
||||
import { Buffer } from 'buffer';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
export interface PrintOptions {
|
||||
ipAddress: string;
|
||||
jobName?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a print job directly and silently to the printer via IPP (Internet Printing Protocol).
|
||||
* Bypasses the OS-level print dialog.
|
||||
*
|
||||
* @param imageUri Local file URI of the JPEG photo to print.
|
||||
* @param options Printer connection settings (IP address, job name).
|
||||
*/
|
||||
export async function printImageLocal(imageUri: string, options: PrintOptions): Promise<void> {
|
||||
const { ipAddress, jobName = 'Schnappix Photo', username = 'Schnappix Kiosk' } = options;
|
||||
|
||||
if (!ipAddress) {
|
||||
throw new Error('Printer IP address is required.');
|
||||
}
|
||||
|
||||
// 1. Read the image file from local storage as Base64 and convert to binary Buffer
|
||||
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// 2. Build the IPP Print-Job request structure
|
||||
const ippRequestObj = {
|
||||
version: { major: 1, minor: 1 },
|
||||
operationId: 0x0002, // Print-Job operation
|
||||
requestId: 1,
|
||||
groups: [
|
||||
{
|
||||
tag: 0x01, // OPERATION_ATTRIBUTES_TAG
|
||||
attributes: [
|
||||
{ tag: 0x47, name: 'attributes-charset', value: 'utf-8' },
|
||||
{ tag: 0x48, name: 'attributes-natural-language', value: 'en-us' },
|
||||
{ tag: 0x45, name: 'printer-uri', value: `ipp://${ipAddress}:631/ipp/print` },
|
||||
{ tag: 0x42, name: 'requesting-user-name', value: username },
|
||||
{ tag: 0x42, name: 'job-name', value: jobName },
|
||||
{ tag: 0x49, name: 'document-format', value: 'image/jpeg' },
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 3. Serialize the IPP metadata structure into a binary buffer
|
||||
const ippHeaderBuffer = ipp.request.encode(ippRequestObj);
|
||||
|
||||
// 4. Concatenate the IPP request buffer with the raw JPEG image data
|
||||
const finalPayload = Buffer.concat([ippHeaderBuffer, imageBuffer]);
|
||||
|
||||
// 5. Send the POST request to the printer via standard HTTP on port 631
|
||||
const printerUrl = `http://${ipAddress}:631/ipp/print`;
|
||||
|
||||
console.log(`Sending silent print job to ${printerUrl}...`);
|
||||
|
||||
const response = await fetch(printerUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/ipp',
|
||||
},
|
||||
// Pass as Uint8Array to ensure React Native fetch treats it as raw binary
|
||||
body: new Uint8Array(finalPayload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Printer responded with HTTP status ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// 6. Read and decode the response from the printer to verify success
|
||||
const responseArrayBuffer = await response.arrayBuffer();
|
||||
const responseBuffer = Buffer.from(responseArrayBuffer);
|
||||
|
||||
try {
|
||||
const decodedResponse = ipp.response.decode(responseBuffer);
|
||||
const statusCode = decodedResponse.statusCode;
|
||||
|
||||
// IPP success status is 0x0000 (successful-ok)
|
||||
if (statusCode !== 0x0000) {
|
||||
throw new Error(`Printer returned IPP error code: 0x${statusCode.toString(16)}`);
|
||||
}
|
||||
console.log('Silent print job accepted successfully!');
|
||||
} catch (e: any) {
|
||||
// If decoding fails, we still assume success since the network request completed successfully
|
||||
console.warn('Failed to parse IPP response, but network request succeeded:', e.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
|
||||
export interface AppSettings {
|
||||
countdownDuration: number; // in seconds
|
||||
printerIp: string;
|
||||
adminPassword: string;
|
||||
kioskModeEnabled: boolean;
|
||||
}
|
||||
|
||||
const SETTINGS_FILE = `${FileSystem.documentDirectory}settings.json`;
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
countdownDuration: 3,
|
||||
printerIp: '192.168.1.100',
|
||||
adminPassword: '1234',
|
||||
kioskModeEnabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Load settings from persistent storage.
|
||||
*/
|
||||
export async function loadSettings(): Promise<AppSettings> {
|
||||
try {
|
||||
const fileInfo = await FileSystem.getInfoAsync(SETTINGS_FILE);
|
||||
if (fileInfo.exists) {
|
||||
const content = await FileSystem.readAsStringAsync(SETTINGS_FILE);
|
||||
const parsed = JSON.parse(content);
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load settings, using defaults:', e);
|
||||
}
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings to persistent storage.
|
||||
*/
|
||||
export async function saveSettings(settings: AppSettings): Promise<void> {
|
||||
try {
|
||||
const content = JSON.stringify(settings, null, 2);
|
||||
await FileSystem.writeAsStringAsync(SETTINGS_FILE, content);
|
||||
console.log('Settings saved successfully:', content);
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const ALBUM_NAME = 'Schnappix';
|
||||
|
||||
/**
|
||||
* Request necessary media library permissions.
|
||||
* @returns Promise<boolean> True if permissions are granted.
|
||||
*/
|
||||
export async function requestStoragePermission(): Promise<boolean> {
|
||||
const { status, canAskAgain } = await MediaLibrary.getPermissionsAsync();
|
||||
if (status === 'granted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (canAskAgain) {
|
||||
const { status: newStatus } = await MediaLibrary.requestPermissionsAsync();
|
||||
return newStatus === 'granted';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a local image file directly to the public DCIM gallery inside the "Schnappix" album.
|
||||
*
|
||||
* @param localUri The local file URI of the image (temp cached file).
|
||||
* @returns Promise<string> The permanent URI of the saved asset in the gallery.
|
||||
*/
|
||||
export async function saveToGallery(localUri: string): Promise<string> {
|
||||
const hasPermission = await requestStoragePermission();
|
||||
if (!hasPermission) {
|
||||
throw new Error('Storage permission not granted. Cannot save photo to gallery.');
|
||||
}
|
||||
|
||||
// 1. Create a media asset from the local file
|
||||
const asset = await MediaLibrary.createAssetAsync(localUri);
|
||||
|
||||
try {
|
||||
// 2. Check if the "Schnappix" album already exists
|
||||
let album = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
|
||||
|
||||
if (!album) {
|
||||
// 3. If it doesn't exist, create it with our asset
|
||||
await MediaLibrary.createAlbumAsync(ALBUM_NAME, asset, false);
|
||||
console.log(`Created new album "${ALBUM_NAME}" and saved photo.`);
|
||||
} else {
|
||||
// 4. If it exists, add our asset to it
|
||||
await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
|
||||
console.log(`Saved photo to existing album "${ALBUM_NAME}".`);
|
||||
}
|
||||
|
||||
return asset.uri;
|
||||
} catch (e: any) {
|
||||
console.error('Error saving asset to album, returning fallback asset URI:', e);
|
||||
return asset.uri;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user