From baf75433f91386cde31248fb331ec77d6fe50bce Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Fri, 26 Sep 2025 22:35:55 +0200 Subject: [PATCH] Basic camera implementaion working --- .envrc | 1 + .gitignore | 3 + Makefile | 233 +++++++++++++++++++++++++++++++++++++ flake.lock | 130 +++++++++++++++++++++ flake.nix | 41 +++++++ src/main.c | 332 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 740 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.c diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f15e8c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out/ +build/ +.direnv/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9cfdee9 --- /dev/null +++ b/Makefile @@ -0,0 +1,233 @@ + +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITARM)/3ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# GRAPHICS is a list of directories containing graphics files +# GFXBUILD is the directory where converted graphics files will be placed +# If set to $(BUILD), it will statically link in the converted +# files as if they were data files. +# +# NO_SMDH: if set to anything, no SMDH file is generated. +# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) +# APP_TITLE is the name of the app stored in the SMDH file (Optional) +# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) +# APP_AUTHOR is the author of the app stored in the SMDH file (Optional) +# ICON is the filename of the icon (.png), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .png +# - icon.png +# - /default_icon.png +#--------------------------------------------------------------------------------- +TARGET := $(notdir $(CURDIR)) +BUILD := build +SOURCES := src +DATA := data +INCLUDES := include +GRAPHICS := gfx +GFXBUILD := $(BUILD) +#ROMFS := romfs +#GFXBUILD := $(ROMFS)/gfx + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft + +CFLAGS := -g -Wall -O2 -mword-relocations \ + -ffunction-sections \ + $(ARCH) + +CFLAGS += $(INCLUDE) -D__3DS__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++14 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lbox2d -lcitro2d -lcitro3d -ljpeg -lctru -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(CTRULIB) $(PORTLIBS) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/out/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) +SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +#--------------------------------------------------------------------------------- +ifeq ($(GFXBUILD),$(BUILD)) +#--------------------------------------------------------------------------------- +export T3XFILES := $(GFXFILES:.t3s=.t3x) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- +export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES)) +export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES)) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ + $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ + $(addsuffix .o,$(T3XFILES)) + +export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) + +export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ + $(addsuffix .h,$(subst .,_,$(BINFILES))) \ + $(GFXFILES:.t3s=.h) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.png) + ifneq (,$(findstring $(TARGET).png,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).png + else + ifneq (,$(findstring icon.png,$(icons))) + export APP_ICON := $(TOPDIR)/icon.png + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_SMDH)),) + export _3DSXFLAGS += --smdh=$(CURDIR)/out/$(TARGET).smdh +endif + +ifneq ($(ROMFS),) + export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) +endif + +.PHONY: all clean + +#--------------------------------------------------------------------------------- +all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + + +$(BUILD): + @mkdir -p $@ + +ifneq ($(GFXBUILD),$(BUILD)) +$(GFXBUILD): + @mkdir -p $@ +endif + +ifneq ($(DEPSDIR),$(BUILD)) +$(DEPSDIR): + @mkdir -p $@ +endif + + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf $(GFXBUILD) + +#--------------------------------------------------------------------------------- +$(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x + +#--------------------------------------------------------------------------------- +else + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS) + +$(OFILES_SOURCES) : $(HFILES) + +$(OUTPUT).elf : $(OFILES) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +.PRECIOUS : %.t3x %.shbin +#--------------------------------------------------------------------------------- +%.t3x.o %_t3x.h : %.t3x +#--------------------------------------------------------------------------------- + $(SILENTMSG) $(notdir $<) + $(bin2o) + +#--------------------------------------------------------------------------------- +%.shbin.o %_shbin.h : %.shbin +#--------------------------------------------------------------------------------- + $(SILENTMSG) $(notdir $<) + $(bin2o) + + +-include $(DEPSDIR)/*.d + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c17892d --- /dev/null +++ b/flake.lock @@ -0,0 +1,130 @@ +{ + "nodes": { + "devkitNix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1752284472, + "narHash": "sha256-6/+VKKBRk1s6LG8SIBP1Te2vOEyAPOAJD8t9E1oFf/w=", + "owner": "bandithedoge", + "repo": "devkitNix", + "rev": "71c8f22a135ff9453f914e6dff2b57524c2cf942", + "type": "github" + }, + "original": { + "owner": "bandithedoge", + "repo": "devkitNix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751949589, + "narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9b008d60392981ad674e04016d25619281550a9d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1752012998, + "narHash": "sha256-Q82Ms+FQmgOBkdoSVm+FBpuFoeUAffNerR5yVV7SgT8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2a2130494ad647f953593c4e84ea4df839fbd68c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devkitNix": "devkitNix", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b63d103 --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + devkitNix.url = "github:bandithedoge/devkitNix"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + devkitNix, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + overlays = [devkitNix.overlays.default]; + }; + in { + devShells.default = + pkgs.mkShell.override { + stdenv = pkgs.devkitNix.stdenvARM; + } { + packages = with pkgs; [imagemagick]; + }; + packages.default = pkgs.devkitNix.stdenvARM.mkDerivation { + name = "somding"; + src = ./.; + buildInputs = [pkgs.imagemagick pkgs.which]; + + # makeFlags = ["TARGET=example"]; + installPhase = '' + mkdir $out + cp 3ds.3dsx $out + ''; + }; + } + ); +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5e3fa2c --- /dev/null +++ b/src/main.c @@ -0,0 +1,332 @@ +#include + +#include <3ds.h> +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 400 +#define HEIGHT 240 +#define FB_SIZE (WIDTH * HEIGHT * 3) +#define STORAGE_DIR "sdmc:/printcam" +#define ENDPOINT_PATH STORAGE_DIR "/printcam/endpoint.txt" +#define API_ENDPOINT_DEFAULT "http://192.168.1.113:8000" +#define JPEG_QUALITY 100 + +static volatile bool exitRequested = false; +static Thread camThread; +static Handle ready[2]; +static u8 *camBuf[2]; +static u8 *rgbBuf; +static volatile u8 writeIdx = 0, dispIdx = 0; +static volatile bool camera_reinit = false; +static volatile int camera_index = 0; +static const u32 cam_selects[2] = {SELECT_OUT1, SELECT_IN1}; +static char api_endpoint[512] = API_ENDPOINT_DEFAULT; +static PrintConsole bottomScreen; + +static bool ensure_storage_dir(void) { + if (mkdir(STORAGE_DIR, 0777) == 0) + return true; + return errno == EEXIST; +} + +static int load_endpoint_from_sd(void) { + FILE *f = fopen(ENDPOINT_PATH, "r"); + if (!f) + return -1; + if (!fgets(api_endpoint, sizeof(api_endpoint), f)) { + fclose(f); + return -1; + } + size_t len = strlen(api_endpoint); + while (len && + (api_endpoint[len - 1] == '\n' || api_endpoint[len - 1] == '\r')) { + api_endpoint[len - 1] = '\0'; + --len; + } + fclose(f); + return 0; +} + +static inline void writePix(u8 *dst, int x, int y, u8 r, u8 g, u8 b) { + int off = (x * HEIGHT + (HEIGHT - 1 - y)) * 3; + dst[off] = b; + dst[off + 1] = g; + dst[off + 2] = r; +} + +static void blitYUYVtoRGB(const u8 *src, u8 *dst) { + for (int y = 0; y < HEIGHT; ++y) { + for (int x = 0; x < WIDTH; x += 2) { + u8 y0 = src[0], u = src[1], y1 = src[2], v = src[3]; + src += 4; + int8_t u2 = (int8_t)(u - 128), v2 = (int8_t)(v - 128); + int r = (int)(1.402f * v2), g = (int)(-0.344f * u2 - 0.714f * v2), + b = (int)(1.772f * u2); + int R = y0 + r, G = y0 + g, B = y0 + b; + R = R < 0 ? 0 : (R > 255 ? 255 : R); + G = G < 0 ? 0 : (G > 255 ? 255 : G); + B = B < 0 ? 0 : (B > 255 ? 255 : B); + writePix(dst, x, y, (u8)R, (u8)G, (u8)B); + R = y1 + r; + G = y1 + g; + B = y1 + b; + R = R < 0 ? 0 : (R > 255 ? 255 : R); + G = G < 0 ? 0 : (G > 255 ? 255 : G); + B = B < 0 ? 0 : (B > 255 ? 255 : B); + writePix(dst, x + 1, y, (u8)R, (u8)G, (u8)B); + } + } +} + +static void convert_display_to_row_major(const u8 *disp, u8 *out) { + for (int y = 0; y < HEIGHT; ++y) { + for (int x = 0; x < WIDTH; ++x) { + int disp_off = (x * HEIGHT + (HEIGHT - 1 - y)) * 3; + int out_off = (y * WIDTH + x) * 3; + out[out_off + 0] = disp[disp_off + 0]; + out[out_off + 1] = disp[disp_off + 1]; + out[out_off + 2] = disp[disp_off + 2]; + } + } +} + +static int encode_bgr_to_jpeg_file(const u8 *in_bgr, int width, int height, + int quality, const char *path) { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + FILE *f = NULL; + unsigned char *row = NULL; + int row_stride = width * 3; + if (!in_bgr || !path) + return -1; + f = fopen(path, "wb"); + if (!f) + return -1; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, f); + cinfo.image_width = width; + cinfo.image_height = height; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + row = malloc(row_stride); + if (!row) { + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + fclose(f); + return -1; + } + while (cinfo.next_scanline < cinfo.image_height) { + const u8 *src = in_bgr + (size_t)cinfo.next_scanline * (size_t)row_stride; + for (int x = 0; x < width; ++x) { + row[x * 3 + 0] = src[x * 3 + 2]; + row[x * 3 + 1] = src[x * 3 + 1]; + row[x * 3 + 2] = src[x * 3 + 0]; + } + JSAMPROW row_pointer[1]; + row_pointer[0] = row; + jpeg_write_scanlines(&cinfo, row_pointer, 1); + } + free(row); + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + fclose(f); + return 0; +} + +Result http_post_file(const char *url, const char *filename) { + Result ret = 0; + httpcContext context; + u32 statuscode = 0; + FILE *f = fopen(filename, "rb"); + if (!f) + return -1; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return -1; + } + long size = ftell(f); + if (size < 0) { + fclose(f); + return -1; + } + rewind(f); + u8 *buf = malloc((size_t)size); + if (!buf) { + fclose(f); + return -1; + } + if (fread(buf, 1, (size_t)size, f) != (size_t)size) { + free(buf); + fclose(f); + return -1; + } + fclose(f); + ret = httpcOpenContext(&context, HTTPC_METHOD_POST, url, 0); + if (ret != 0) { + free(buf); + return ret; + } + httpcSetSSLOpt(&context, SSLCOPT_DisableVerify); + httpcSetKeepAlive(&context, HTTPC_KEEPALIVE_ENABLED); + httpcAddRequestHeaderField(&context, "User-Agent", "printcam/1.0"); + httpcAddRequestHeaderField(&context, "Content-Type", "image/jpeg"); + ret = httpcAddPostDataRaw(&context, (u32 *)buf, (u32)size); + if (ret != 0) { + httpcCloseContext(&context); + free(buf); + return ret; + } + ret = httpcBeginRequest(&context); + if (ret != 0) { + httpcCloseContext(&context); + free(buf); + return ret; + } + ret = httpcGetResponseStatusCode(&context, &statuscode); + (void)statuscode; + httpcCloseContext(&context); + free(buf); + return ret; +} + +static void cameraThreadFunc(void *arg) { + acInit(); + camInit(); + u32 sel = cam_selects[camera_index]; + CAMU_SetSize(sel, SIZE_CTR_TOP_LCD, CONTEXT_A); + CAMU_SetOutputFormat(sel, OUTPUT_YUV_422, CONTEXT_A); + CAMU_SetFrameRate(sel, FRAME_RATE_30); + CAMU_SetNoiseFilter(sel, true); + CAMU_SetAutoExposure(sel, true); + CAMU_SetAutoWhiteBalance(sel, true); + u32 maxBytes; + CAMU_GetMaxBytes(&maxBytes, WIDTH, HEIGHT); + CAMU_SetTransferBytes(PORT_BOTH, maxBytes, WIDTH, HEIGHT); + CAMU_Activate(sel); + Handle ev = 0; + CAMU_ClearBuffer(PORT_BOTH); + CAMU_SynchronizeVsyncTiming(SELECT_OUT1, SELECT_OUT2); + CAMU_StartCapture(PORT_BOTH); + while (!exitRequested) { + if (camera_reinit) { + sel = cam_selects[camera_index]; + CAMU_StopCapture(PORT_BOTH); + CAMU_ClearBuffer(PORT_BOTH); + CAMU_SetSize(sel, SIZE_CTR_TOP_LCD, CONTEXT_A); + CAMU_SetOutputFormat(sel, OUTPUT_YUV_422, CONTEXT_A); + CAMU_SetFrameRate(sel, FRAME_RATE_30); + CAMU_SetNoiseFilter(sel, true); + CAMU_SetAutoExposure(sel, true); + CAMU_SetAutoWhiteBalance(sel, true); + CAMU_SetTransferBytes(PORT_BOTH, maxBytes, WIDTH, HEIGHT); + CAMU_Activate(sel); + CAMU_StartCapture(PORT_BOTH); + camera_reinit = false; + } + CAMU_SetReceiving(&ev, camBuf[writeIdx], PORT_CAM1, WIDTH * HEIGHT * 2, + maxBytes); + svcWaitSynchronization(ev, U64_MAX); + svcCloseHandle(ev); + svcSignalEvent(ready[writeIdx]); + writeIdx ^= 1; + } + CAMU_StopCapture(PORT_BOTH); + CAMU_Activate(SELECT_NONE); + camExit(); + acExit(); +} + +int main(int argc, char *argv[]) { + gfxInitDefault(); + httpcInit(4 * 1024 * 1024); + consoleInit(GFX_BOTTOM, &bottomScreen); + consoleSelect(&bottomScreen); + printf("printcam\nL/R=take X=switch-front/back Y=reload-url START=exit\n"); + if (!ensure_storage_dir()) + printf("Failed to create %s\n", STORAGE_DIR); + if (load_endpoint_from_sd() == 0) + printf("Endpoint: %s\n", api_endpoint); + camBuf[0] = (u8 *)memalign(0x40, WIDTH * HEIGHT * 2); + camBuf[1] = (u8 *)memalign(0x40, WIDTH * HEIGHT * 2); + rgbBuf = (u8 *)memalign(0x40, FB_SIZE); + if (!camBuf[0] || !camBuf[1] || !rgbBuf) { + printf("Out of memory\n"); + goto cleanup; + } + svcCreateEvent(&ready[0], RESET_ONESHOT); + svcCreateEvent(&ready[1], RESET_ONESHOT); + gfxSetDoubleBuffering(GFX_TOP, true); + camThread = threadCreate(cameraThreadFunc, NULL, 0x1000, 0x2F, -2, false); + u8 *linearBuf = malloc(FB_SIZE); + while (aptMainLoop()) { + hidScanInput(); + u32 k = hidKeysDown(); + if (k & KEY_START) + break; + if (k & KEY_X) { + camera_index ^= 1; + camera_reinit = true; + printf("Switched to %s\n", + camera_index ? "front/input1" : "back/output1"); + } + if (k & KEY_Y) { + if (load_endpoint_from_sd() == 0) + printf("Reloaded endpoint: %s\n", api_endpoint); + else + printf("Failed to reload endpoint\n"); + } + if (k & (KEY_L | KEY_R)) { + consoleSelect(&bottomScreen); + printf("Snapshot requested\n"); + convert_display_to_row_major(rgbBuf, linearBuf); + char jpgpath[128]; + time_t t = time(NULL); + strftime(jpgpath, sizeof(jpgpath), STORAGE_DIR "/%Y%m%d_%H%M%S.jpg", + localtime(&t)); + if (encode_bgr_to_jpeg_file(linearBuf, WIDTH, HEIGHT, JPEG_QUALITY, + jpgpath) == 0) { + printf("Wrote %s\n", jpgpath); + Result res = http_post_file(api_endpoint, jpgpath); + printf("http_post_file returned: %" PRIx32 "\n", (u32)res); + if (res == 0) + remove(jpgpath); + } else { + printf("JPEG write failed\n"); + } + } + gspWaitForVBlank(); + if (svcWaitSynchronization(ready[dispIdx], 0) == 0) { + blitYUYVtoRGB(camBuf[dispIdx], rgbBuf); + dispIdx ^= 1; + } + u8 *fb = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL); + memcpy(fb, rgbBuf, FB_SIZE); + gfxSwapBuffersGpu(); + } +cleanup: + exitRequested = true; + threadJoin(camThread, U64_MAX); + threadFree(camThread); + svcCloseHandle(ready[0]); + svcCloseHandle(ready[1]); + free(camBuf[0]); + free(camBuf[1]); + free(rgbBuf); + free(linearBuf); + httpcExit(); + gfxExit(); + return 0; +}