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:
2026-05-25 16:44:15 +02:00
parent 5958319772
commit 872e1ffc86
51 changed files with 8931 additions and 1 deletions
+92
View File
@@ -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);
}
}
+48
View File
@@ -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;
}
}
+58
View File
@@ -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;
}
}