Skip to the content
4 layers110 × 90 mmFilMachine Mainboard, the printed circuit board in 3D

FilMachine Mainboard

yesnoj/FilMachine_Reworked

FilMachine is an automated film processing machine designed for photographic film development. It handles the entire process: chemical baths, water rinses, temperature regulation, and motor-driven film agitation — all…

Size
110 × 90 mm
Copper layers
4
Thickness
1.6 mm
Parts
205
Nets
118
Pads
571
Tracks, vias
1,666, 575
Pours
26
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
17 Mar 2026
Updated
23 Oct 2026

Overview

FilMachine Mainboard is an open-source printed circuit board by yesnoj, from the yesnoj/FilMachine_Reworked repository on GitHub. It is a 4-layer board of 110 × 90 mm with 205 parts on 118 nets, routed with 1,666 tracks and 575 vias.

It is built around the BTN8962TAAUMA1, ADS1115IDGSR and DRV8871DDARQ1, with the LMR33630ADDA, UCC27524DR and SN74LVC2G08DCTR among its other parts. Its parts include 71 resistors, 38 capacitors, 38 diodes, 26 connectors and 10 transistors. It belongs with the computers and single-board computers boards of this library.

Main components

12

README

README.md

FilMachine — Automated Film Development Machine

FilMachine is an automated film processing machine designed for photographic film development. It handles the entire process: chemical baths, water rinses, temperature regulation, and motor-driven film agitation — all controlled through a 4.3" color touchscreen display.

The project includes a desktop simulator (SDL2 + LVGL) that reproduces the full touchscreen UI on macOS/Linux (a Windows build is produced by CI), enabling rapid development and testing without physical hardware, plus an automated test suite (19 suites). A Flutter companion app connects via WebSocket and provides full remote control from any mobile device on the same network.

The interface is bilingual (English / Italiano) — selectable from Settings, applied with an automatic reboot — and every boot writes a diagnostic log to the SD card (/log/boot_NNNN.txt), including the crash summary of the previous run when the firmware panicked.

The repository also holds the mechanical side of the project: the 3D-printed film reel auto-loader (bench prototype v1, for 135 and 120 film) — see Film Reel Auto-Loader.


Table of Contents

  1. What is FilMachine?
  2. Project Architecture
  3. Directory Structure
  4. Building the Project
  5. Running the Simulator
  6. Running the Tests
  7. WebSocket Server & Remote Control
  8. Flutter Companion App
  9. User Interface Guide
  10. Firmware Update (OTA)
  11. Hardware Specifications
  12. Bill of Materials (Components)
  13. Film Reel Auto-Loader (3D-Printed Prototype)
  14. Diagnostics & Bring-up Page
  15. Build-time Switches
  16. Configuration & Persistence
  17. Language / Localization
  18. SD Boot & Crash Log
  19. Typical Workflows
  20. Developer Quick Reference
  21. Authors

What is FilMachine?

FilMachine takes the guesswork out of developing photographic film at home or in a small lab. Instead of manually timing each step, pouring chemicals by hand, and constantly checking the thermometer, FilMachine automates everything. You load your film in the developing tank, select a process, and press Start. The machine takes care of the rest: filling the right chemical at the right time, keeping the temperature stable, rotating the film at the correct speed, rinsing between steps, and alerting you when the process is complete.

It supports both color negative (C41, E6) and black & white film development workflows, with full customization of every parameter.


Project Architecture

The project has a dual-target build system that produces ESP32-P4 firmware or a native desktop application from the same codebase. The only supported hardware board is the Guition JC4880P433 (ESP32-P4).

Technology Stack

LayerFirmware (ESP32-P4)Simulator (macOS/Linux)
UI LibraryLVGL 9.5.0LVGL 9.5.0 (identical)
DisplayST7701S 480×800 MIPI-DSI → landscape 800×480 via PPASDL2 window (800×480)
Touch InputGT911 (physical 480×800 → remapped to 800×480)SDL2 mouse events
StorageFatFS on MicroSD (SDMMC 4-bit, ~40 MB/s)POSIX file I/O (sd/ directory)
RTOSFreeRTOS (ESP-IDF)Stub (queues work, tasks are no-ops)
2D AccelerationPPA hardware engine (rotate, scale, blend, fill)—
AudioES8311 codec via I2S + power amplifier—
Temp SensorsDS18B20 OneWireSimulated (20°C ambient, heater model)
Motor/PumpDBH-12V dual H-bridge (motor ch.A + pump ch.B) + MCP23017 solenoid driverprintf stubs
OTA Updatesesp_ota_ops + esp_http_serverSimulated (progress timer)
Build ToolESP-IDF (idf.py)CMake + Make
SensorsFlow meter, water level, hall effectSimulated
Compilerriscv32-esp-elf-gccNative cc/gcc/clang

How the Simulator Works

The simulator replaces all hardware-specific code with a stub layer that provides compatible implementations:

  • FatFS stubs map firmware paths (e.g., /FilMachine.json) to files inside an sd/ subdirectory of the directory the simulator is launched from, so read/write operations work transparently.
  • FreeRTOS stubs provide working queue implementations (xQueueCreate, xQueueSend, xQueueReceive) using simple circular buffers. Task creation is a no-op since there are no real threads — instead, the main loop drains the system queue every frame.
  • Hardware stubs (GPIO, I2C, SPI, solenoid driver, motor, temperature) are either no-ops or printf-based logging functions. Temperature readings come from a simple thermal model that simulates heating/cooling.
  • OTA stubs simulate the firmware update flow with a progress timer (0→100%) and fake IP address, allowing full UI testing without real hardware.
  • LVGL's SDL2 driver provides the display backend and mouse input, which simulates the capacitive touchscreen.

The key architectural principle is that all UI code is shared 1:1 between firmware and simulator. Pages, elements, event handlers, and business logic are identical — only the hardware abstraction layer changes.


Directory Structure

text
FilMachine_Reworked/
│
├── main/                          # Core source (shared with ESP32 firmware)
│   ├── include/
│   │   └── FilMachine.h           #   Main header — all structs, enums, constants, prototypes
│   ├── FilMachine.c               #   ESP32 entry point — board-conditional display/touch init
│   ├── accessories.c              #   Utilities — linked lists, deep copy, config I/O, keyboard
│   ├── lang.c                     #   EN/IT string table (generated by scripts/gen_lang.py)
│   ├── sd_log.c                   #   Boot/crash logger on SD (/log/boot_NNNN.txt, firmware only)
│   ├── mini_json.c                #   Dependency-free JSON parser for the config file
│   ├── ota_update.c               #   OTA firmware update (SD card + Wi-Fi web server)
│   ├── ws_server.c                #   WebSocket server for Flutter companion app
│   ├── film_loader.c              #   Film loading cycle (wind, stall detect, cut, tail) — Tools → Load film
│   └── ui_profile.c               #   Centralized UI layout constants (800×480)
│
├── c_pages/                       # UI pages (screens)
│   ├── page_splash.c              #   Splash screen — standard, random, and custom modes with
│   │                              #     10 palettes, 6 shape styles, 9 title fonts, PRNG engine
│   ├── page_menu.c                #   Tab bar navigation (Processes / Settings / Tools)
│   ├── page_processes.c           #   Process list with filtering
│   ├── page_processDetail.c       #   Process creation & editing
│   ├── page_stepDetail.c          #   Step creation & editing with validation
│   ├── page_settings.c            #   Machine settings, grouped in sections (process, rotation, liquids, display, system)
│   ├── page_tools.c               #   Maintenance tools, import/export, statistics, OTA

Building the Project

Prerequisites

macOS:

bash
brew install cmake sdl2 pkg-config

Ubuntu / Debian:

bash
sudo apt install cmake libsdl2-dev pkg-config build-essential

LVGL v9.5.0 is automatically cloned from GitHub on the first build if not present (the firmware pulls the same version through the ESP-IDF component manager, lvgl/lvgl ~9.5.0).

Windows: there is no local recipe; the GitHub Actions workflow .github/workflows/windows-sim.yml builds filmachine_sim.exe with MSYS2/MinGW on every push to main that changes more than the documentation (or on demand from the Actions tab) and uploads it — with its runtime DLLs and a freshly generated sd/ config — as the filmachine-sim-windows artifact.

Build the Simulator

bash
mkdir -p build800 && cd build800
cmake ..
make filmachine_sim

The simulator opens an 800×480 window matching the JC4880P433 physical panel in landscape orientation.

Build the Tests

bash
mkdir -p build800 && cd build800
cmake ..
make filmachine_test
./filmachine_test

Build the Firmware

Requires the ESP-IDF toolchain (v5.5.x or later) installed and configured.

Important: run idf.py from the project root, not from build800/.

bash
cd FilMachine_Reworked
. $HOME/esp/esp-idf-v5.5/export.sh     # Must be ESP-IDF 5.5.x or later

# First-time setup: set target to esp32p4 (creates sdkconfig from defaults)
idf.py set-target esp32p4

# Build — board is auto-detected from IDF_TARGET
idf.py build

The main/CMakeLists.txt automatically sets -DBOARD_JC4880P433. No manual -DCMAKE_C_FLAGS flag is needed (passing -D CMAKE_C_FLAGS="-DBOARD_JC4880P433" is harmless but redundant).

See COMMANDS.md for the canonical quick reference: simulator + board build, flash/monitor, generating and copying the SD config, version bumping and cleanup.

Flash and monitor

The firmware binary is produced at build/FilMachine.bin. To flash it directly to a connected board:

bash
idf.py flash

Or use the all-in-one helper (written for macOS), which sets the target if needed, patches sdkconfig for the P4 board, increments the build number in version.txt, builds, looks for the serial port, flashes and opens the monitor:

bash
./flash_p4.sh

flash.sh and setup.sh are leftovers of the earlier ESP32-S3 workflow (they copy sources between the old FilMachine_Simulator_v2 folder and the FilMachine_Pete repository). They are not used for the ESP32-P4 and can be ignored.

P4 note: the bootloader offset on ESP32-P4 is 0x2000 (different from ESP32-S3's 0x0). This is handled automatically by idf.py when the target is set correctly.

To check the firmware size (useful for verifying OTA partition fit):

bash
idf.py size

Running the Simulator

Simulator UI debug overlay

The simulator includes a built-in UI inspection mode to speed up layout tuning.

  • Press F2 to enable or disable the overlay.
  • When enabled, moving the mouse over the UI shows:
    • the resolved component name when available (for example gui.page.processes.newProcessButton)
    • the object position (x, y)
    • the object size (w, h)
  • While the overlay is enabled, right click dumps the hovered object to the terminal/console.

This is especially useful when adjusting coordinates inside ui_profile.c, because you can identify the exact widget and immediately see its current geometry in the simulator.

bash
cd build800
./filmachine_sim

The simulator opens an 800×480 window that reproduces the exact touchscreen interface. Use the mouse to simulate touch input (click = tap, click-drag = swipe/scroll).

What works in the simulator

  • Full UI navigation (all pages, popups, dialogs)
  • Splash screen with live preview popup, 10 palettes, 6 shape styles, 9 custom fonts
  • Process creation, editing, duplication, deletion
  • Step management with swipe gestures
  • Settings with slider/switch/radio controls
  • Filter and search functionality
  • Configuration save/load (reads/writes sd/FilMachine.json)
  • Export/Import (backup to sd/FilMachine_Backup.json)
  • Drain machine with animated tank-level bars and relay management
  • Empty chemistry (machine v7): C1, C2 or C3 into its recovery bottle through the pump and the recovery outlet, line washed before and after, with a source → tank → bottle animation
  • Clean machine with per-container rinse cycles and arc progress
  • Self-check diagnostic wizard with 8-phase hardware test simulation (temp sensors, pump, heater, valves, containers, motor)
  • OTA update UI (SD card check + Wi-Fi popup with simulated IP/PIN/progress)
  • Simulated temperature readings with heater model and calibration offset
  • Persistent alarm sound (SDL audio 880Hz beep, repeats every 10s until dismissed)
  • Drain/fill overlap processing (adjusts step timing based on overlap percentage)
  • Machine statistics persistence (saved in config file, survives restart)
  • Temperature sensor calibration (Tune button calculates and applies offset)
  • Console logging of all system actions ([SIM] sysAction: ...)
  • WebSocket server on port 81 for Flutter companion app remote control
  • Full remote process start, stop, and checkup advance via WebSocket
  • Remote CRUD for processes and steps (create, edit, delete from Flutter)

What is simulated (no real hardware)

  • Temperature sensors return simulated values (20°C ambient, gradual heating/cooling)
  • Motor control outputs to console only
  • Relay switching outputs to console only
  • OTA writes to a fake partition (progress simulated, no actual flash)
  • Wi-Fi connection is simulated (fake IP 192.168.1.42, no real server)
  • Reboot is a no-op (prints message)

Generating sample data

To generate realistic film development recipes:

bash
python3 scripts/genFilMachineCFG.py --realistic --output build800/sd/

This creates config files with authentic C41, E6, and B&W development processes.


Running the Tests

bash
cd build800
./filmachine_test

Results are displayed in the terminal and saved to test_results/test_results_YYYYMMDD_HHMMSS.txt.

Test Suites

SuiteDescription
NavigationSplash screen, menu display, tab switching
ProcessesProcess list rendering, creation UI
Process CRUDFull create/read/update/delete lifecycle
StepsStep creation, swipe gestures, deletion
Step CRUDStep lifecycle with validation
ExecutionProcess checkup and execution flow
PersistenceConfig save → reload → verify all fields, stats persistence, calibration offset
SettingsSettings UI, default values, slider/switch behavior, calibration, alarm
FilterFilter by name, film type, preferred flag
ToolsCleaning, draining, statistics display, timer pause/resume, alarm functions
Edge CasesBoundary conditions, max limits, error recovery
UtilitiesHelper functions, linked list operations, drain/fill overlap calculation
UI Profile & SensorsProfile values match original, popup dimensions, font pointers, sensor stubs, board constants
OTAOTA update UI, Wi-Fi popup PIN generation, progress popup
WebSocketWebSocket command handling, async LVGL dispatch
New SettingsExtended settings validation
Self-checkSelf-check diagnostic wizard phases
LiveSyncLive sync between WebSocket state and LVGL UI
Destroy & LifecycleMemory cleanup, object destruction
Film loaderLoading cycle 135/120, early stall / no spin / too long errors, Cut now, Stop, blade test, popup from the app
Maintenance from the appDrain and Clean started, stopped and closed by WebSocket commands (popups on the display, state fields), only the selected containers cleaned, one tool at a time, no process while one runs, Export queued with its result in the state

The persistence tests verify that every field (process name, temperature, tolerance, film type, preferred flag, step names, durations, types, sources, discard flags) survives a full save-and-reload cycle.

Repeatable runs. Every run starts from the same data: the runner generates three test processes with the default settings and rewrites sd/FilMachine.json in the run directory, so a run never depends on what the previous one saved (steps used to pile up on the first process until the 30-step limit made the step tests fail). FM_TEST_ONLY=<suite>,<suite> runs only some suites (names after test_suite_, e.g. FM_TEST_ONLY=navigation,tools).

Timers run on real time. The SDL display makes LVGL read the real clock, so test_pump() processes events but does not advance LVGL timers; tests that wait for a 1 s timer sleep (see pump_real() in test_maintenance_remote.c).

Memory. The simulator and the tests use the C-library malloc for LVGL, like the board. With LVGL's own 256 KB pool the heap fragmented after a few suites, an allocation failed and LVGL's malloc assert spun forever, which looked like random hangs in the step tests. To check memory errors, build the runner with -fsanitize=address: AddressSanitizer then sees LVGL's allocations too.


WebSocket Server & Remote Control

The simulator and firmware include a built-in WebSocket server (ws_server.c) that enables remote control from the Flutter companion app or any WebSocket client. The server starts automatically and listens on port 81 at path /ws.

Supported Commands

All commands use JSON format: {"cmd":"command_name", ...params}.

CommandParametersDescription
get_state—Returns full machine state (settings, runtime, temperatures, progress, alarms, maintenance fill, film loader, drain, clean and export status)
get_processes—Returns complete process list with steps, indexed for remote referencing
start_processindexStart a process by list index; initializes checkup and begins execution
checkup_advance—Advance to next checkup phase (Setup → Fill → Temp → Check → Processing)
stop_now—Immediately halt the running process and drain
stop_after—Stop after the current step completes
close_process—Close a finished/stopped process and free checkup resources
create_processname, temp, tolerance, filmType, tempControlled, preferredCreate a new process
edit_processindex, name, temp, tolerance, filmType, tempControlled, preferredEdit an existing process
delete_processindexDelete a process
add_stepprocessIndex, name, mins, secs, type, source, discardAdd a step to a process
edit_stepprocessIndex, stepIndex, name, mins, secs, type, source, discardEdit a step
delete_stepprocessIndex, stepIndexDelete a step
reorder_stepprocessIndex, from, toMove a step within a process
set_settingkey, valueUpdate a machine setting (see key list below)
reset_defaults—Restore all settings to factory defaults
wifi_scan—Scan for Wi-Fi networks; results arrive via a wifi_scan_results event
fill_start / fill_stoptarget (0 = water bath, 1 = chemistry)Maintenance fill (self-calibrates the fill time; refused with fill_rejected while a process runs)
tune_test / tune_set / tune_cancelkind (0 pump, 1 motor, 2 volume), percent, onLive test, store or cancel a Tune value
drain_start / drain_stop / drain_close—Drain machine: start (no confirm page, the app asks), Stop, Close after the end (alarm off, popup closed). Refused with drain_rejected while a process or another maintenance tool runs
recover_start / recover_stop / recover_closecontainer (0 = C1, 1 = C2, 2 = C3)Empty that chemistry into its recovery bottle (machine v7): wash the line, discard the head, transfer, wash again. Progress in recoverToolState (0 idle, 1 running, 2 stopping, 3 done, 4 stopped), recoverToolPhase (0..3), recoverToolPct, recoverToolRemaining, recoverToolMovedMl. Refused with recover_rejected while a process or another tool runs; a drain still finishing is waited for
clean_startmask (bit0 C1, bit1 C2, bit2 C3), cycles (1–5), drainWbClean machine with these choices (shown on the display popup). Refused with clean_rejected (processing, busy, no_container)
clean_stop / clean_close—Clean: Stop (the container is pumped back first) / Close after the end
export_config—Export configuration and processes to the backup file on the SD card; the result arrives as exportSeq / exportOk in the state
load_startformat (0 = 135, 1 = 120)Start loading the film onto the reel (refused with a load_rejected event while a process runs or a load is in progress)
load_stop—Stop the loading (motor off, blade back to rest)
load_cut—Cut now, without waiting for the reel to stop
cutter_testangle (0–180)Hold the cutter servo at that angle for a few seconds (setup)
cutter_cycle—One blade up/down cycle, motor off

The cleanTool* state fields include cleanToolProcessArc, cleanToolCycleArc and cleanToolPumpArc: the values of the three arcs exactly as drawn on the display's Clean popup, so the app draws the same picture.

set_setting keys — tempUnit, waterInlet, tempCalibOffset, chemCalibOffset, filmRotationSpeed, rotationInterval, random, persistentAlarm, autostart, drainFillOverlap, multiRinseTime, lineRinseEnabled, lineRinseTime, tankSize, pumpSpeed, chemCalibFillSecs, wbCalibFillSecs, chemistryVolume, invertPump, brightness, volume, splashDefault, splashRandom, splashPalette, splashShapeStyle, splashComplexity, language (0=EN, 1=IT — applied at next boot), screenOffMins (5/10/30, 0=never), wifiEnabled, loadSpeed (reel motor % while loading), cutterRestUs / cutterCutUs (servo pulse in µs at rest and at the top of the cut). All of these are also included in the broadcast state JSON.

Implementation Details

The server uses simple strstr() JSON parsing with no external library (no cJSON, no heap allocation for parsing). All commands that modify LVGL state are dispatched via lv_async_call() to ensure thread-safe UI updates. State changes are automatically broadcast to all connected clients.


Flutter Companion App

The companion app is a Flutter application that provides full remote control of FilMachine from any mobile device or desktop on the same network. It lives in its own repository, FilMachine_App (private); see its README for detailed documentation.

Keeping the two in step. The app mirrors the protocol implemented in main/ws_server.c. Whenever a field is added to build_state_json() or a key to set_setting, the app needs the same addition in its MachineState model, in MachineService._readSetting and in its Settings screen. The app release synced with firmware v0.0.0.54 parses all 71 state fields and sends all 17 commands; the only set_setting keys it never sends are the fill calibrations (read-only in the app) and wifiEnabled.

Key Features

  • Device Discovery: Automatic mDNS discovery of FilMachine devices on the local network (_filmachine._tcp), plus manual IP/port entry
  • Process Management: Create, edit, duplicate, and delete processes and steps — all changes sync to the machine in real-time
  • Live Execution Monitoring: Real-time dashboard showing current step, progress bars, temperatures, tank fill status, and elapsed/remaining time
  • Process Control: Start processes, advance through checkup phases, Stop Now / Stop After with confirmation dialogs
  • Filtering: Client-side filtering by name, film type (B&W / Color), and preferred flag
  • Statistics: View completed processes, stopped processes, total development time, cleaning cycles
  • Settings: Full access to the machine settings, with the same sections as the display (Process, Film rotation, Pump and liquids, Display and sound, System) plus the app's own options
  • Tools: A Tools tab laid out like the display's: Maintenance (Clean machine, Drain machine, Fill bath, Fill chem, Load film), Utilities (Export) and Software (Credits). Self-check, Import (it reboots the machine) and the firmware updates (the Wi-Fi one shows a PIN on the display on purpose) stay on the display; statistics, version and serial number are on the app's Dashboard
  • Theme: Dark/light theme toggle with custom FilMachine color palette
  • Persistent Connection: Remembers last successful connection for quick reconnect

Architecture

The app uses Provider for state management. A central MachineService (ChangeNotifier) maintains the WebSocket connection and holds the current MachineState — a data class with one field per key of the JSON state broadcast. All screens rebuild reactively when state changes.


User Interface Guide

The Processes Tab

This is where you manage your film development recipes. Each process is a sequence of steps the machine executes in order. The list shows each process with its name, total time, film type icon, and a star if marked as preferred.

  • Process count: The section header dynamically shows the number of processes (e.g., "8 Processes"). The "+" button stays positioned right after the label text regardless of the count.
  • Create: Tap "+" to add a new process
  • Edit: Tap a process to open its detail view
  • Duplicate: Swipe left to reveal the duplicate button
  • Delete: Open the detail, tap the trash icon
  • Filter: Tap the filter icon to search by name, film type, or preferred status. The filter icon turns green when a filter is active, and returns to white when reset.

Working with Steps

Each process contains steps. A step represents one phase: "Developer", "Stop Bath", "Fixer", "Wash", etc.

For each step you configure: name, duration (minimum 30 seconds), chemical type (Chemistry / Rinse / Multi-Rinse), source container (C1, C2, C3, or WB), and whether to discard the chemical after use.

The three step types behave differently during execution: a Chemistry step fills once from a container and soaks for the whole step time (liquid recovered or discarded per the flag). A Rinse step fills once (from any source), soaks, and always discards to waste at the end. A Multi-Rinse step is not a single soak: the tank is repeatedly filled with fresh water (always from the water bath), agitated for one cycle — the "Multi-rinse cycle time" setting — then fully drained to waste, over and over until the step's total duration elapses. If the total time isn't an exact multiple of the cycle time, the last cycle runs longer so the step still ends exactly on time. This matches the Adeo dev.a behaviour and is ideal for final washes where the water must be changed frequently.

  • Add: Tap "+" below the step list
  • Edit: Tap a step to modify it
  • Duplicate: Swipe left on a step
  • Delete: Swipe left to reveal the delete button
  • Reorder: Long-press and drag

Running a Process (Checkup)

Open a process and tap Play. The machine walks through pre-flight checks, then executes each step automatically.

Pre-flight checks: tank size selection, water bath fill, tank presence verification, motor check, and temperature stabilization (for temperature-controlled processes).

During execution: the screen shows the current step name and remaining time, chemical source, current/target temperature, and upcoming steps.

Stopping: "Stop now!" halts immediately (potentially unsafe). "Stop after!" waits for the current step to finish — the safer choice.

Splash Screen

The splash screen appears at boot and supports three modes, configured via Settings → Splash Screen:

Use Default — The built-in "Deep Ocean" splash: a hand-tuned composition with a teal-to-navy gradient, geometric shapes, and the "FILMACHINE" title in Montserrat 48. This is the factory default.

Random next boot — Each boot generates a completely new splash from scratch using a tick-derived seed. The PRNG (xorshift32) deterministically selects a palette, shape style, complexity, title position, and title font — producing a unique visual identity every time the machine starts.

Custom — Fine-tune the splash parameters manually: Palette (10 options), Shape Style (6 options), and Complexity (20–100 in steps of 20). The popup shows a live preview of the current configuration as the background behind its own controls, with a dark overlay for readability. Press the Random button to regenerate all parameters at once; if you like the result, it will be applied at next boot. The preview shows only the shape background (no title or play button) so you can evaluate the pattern clearly.

Title fonts — In Random and Custom modes, the title "FILMACHINE" is rendered with one of 9 fonts selected by the PRNG:

IndexFont NameStyle
0Montserrat 48Default sans-serif (same as Deep Ocean)
1Air AmericanaBold italic display
2Decaying Felt PenHand-drawn brush effect
3DS DigitalLED/LCD digital display
4EvanescentEthereal and elegant
5Nerdropol LatticeGeometric/tech
6RetrolightRetro/vintage
7Tropical LeavesDecorative botanical
8Wishful MelisandeCalligraphic script

All fonts are converted from TTF/OTF to LVGL .c bitmap arrays using lv_font_conv with --no-compress --bpp 4 --range 0x20-0x7F (ASCII only, ~20–40KB each in flash).

Palettes — Cyberpunk, Aurora, Lava, Deep Ocean, Forest, Sunset, Machinery, Arctic, Neon, Pastel. Each defines a top/bottom gradient, 3 shape accent colors, and text/accent colors for the title.

Shape Styles — Overlapping Rects, Circles & Arcs, Mixed Shapes, Diagonal Bands, Grid Blocks, Radial Arcs.

The Settings Tab

The settings are grouped in sections, like the Tools tab; the whole page scrolls with its section titles. The app's Settings screen uses the same groups and the same wording.

SectionSettingDescriptionRange
ProcessProcess autostartAuto-start when temperature reachedOn/Off
ProcessPersistent alarmAlarm sounds until acknowledgedOn/Off
ProcessTemperature unit°C or °F—
ProcessTemp sensor calibrationCalibrate against a reference thermometer. Short-press Tune to set ambient temp, long-press to reset.Tune button
Film rotationRotation speedFilm agitation motor RPM10–100%
Film rotationInversion intervalSeconds between motor direction changes10–60s
Film rotationRandomnessRandom variation on inversion interval0–100%
Pump and liquidsTank sizeDefault developing tank sizeS (500ml) / M (700ml) / L (1000ml)
Pump and liquidsChemistry volumeAmount of chemistry used per stepLow / High
Pump and liquidsPump speedPeristaltic pump speed (Kamoer KPHM600: 600 ml/min at 100 %); every fill/drain time is computed from it10–100% (default 100)
Pump and liquidsInvert pumpInvert H-bridge direction to compensate for pump physical switch positionOn/Off
Pump and liquidsWater inletAutomatic water fill if connectedOn/Off
Pump and liquidsDrain/fill overlapHow much of fill/drain time counts as processing time (100% recommended)0–100%
Pump and liquidsMulti-rinse cycle timeDuration of each rinse in multi-rinse steps60–180s
Pump and liquidsLine rinseFlush the shared pump line with water after each chemistry stepOn/Off
Pump and liquidsLine rinse timeDuration of the line-rinse flush5–60s
Display and soundBrightnessLCD backlight brightness (auto-dim after inactivity; board only)10–100%
Display and soundVolumeSpeaker volume0–100%
Display and soundScreen off timeoutMinutes after the last touch before the screen turns off (dims in two steps first; disabled while a process runs)5 / 10 / 30 min / Never
Display and soundLanguageInterface language — the machine saves and reboots automatically to apply itEnglish / Italiano
SystemSplash ScreenOpens a popup to configure the boot splash (see above)Default / Random / Custom
SystemWi-FiOpens the Wi-Fi popup: scan, connect, status (used for OTA updates and the app)—
SystemReset to DefaultsRestores every setting to its factory value—

The fill times are not settings any more: they are measured by the Maintenance fills in the Tools tab. The film loader's motor speed and cutter servo pulses (loadSpeed, cutterRestUs, cutterCutUs) are stored with the settings but are not shown on this page; they are set from the app's Load film screen (or in FilMachine.json).

All settings are saved automatically to the SD card when changed. Slider values are saved only when you release the slider (not during dragging) to reduce SD card wear.

The Tools Tab

  • Clean machine — Automated cleaning cycle: select which containers to clean (C1, C2, C3), set the number of rinse cycles (1–5), and optionally drain the water bath when done. Only the selected containers are cleaned, in order: each cycle fills the container with water from the water bath and then pumps it back, with real-time progress shown via arc animations and remaining-time countdown. Stop pumps the water still in the current container back to the bath before ending. It can also be started, stopped and closed from the app (the popup shows the app's choices).
  • Empty chemistry (machine v7) — One row: the popup asks which container (C1, C2 or C3) and moves its whole content into its recovery bottle through the RECOVERY outlet (valve A6, the seventh port of the collector). Four phases, all through the tank: bath water through the line to waste, the first 30 ml of the chemistry to waste (they carry away the water film), the chemistry into the bottle cycle after cycle (until its MIN sensor pulls air, or 1 litre), bath water through the line again. Nothing but that chemistry and a few tenths of a millilitre of water can reach the bottle. The popup shows source → tank → bottle bars with a drop running along the pipe, the four phases as dots, the remaining time; Stop drains the tank to where the liquid was going and ends the run. Also from the app (recover_start).
  • Drain machine — Drains all containers (C1, C2, C3, WB) to waste sequentially. A confirmation screen lists the affected containers; once started, four colored tank bars animate from full to empty in real time, showing which container is currently draining, a ">> WASTE <<" indicator, and a countdown timer. The drain can be stopped at any time via the Stop button. It can also be started, stopped and closed from the app.
  • Self-check — Guided hardware diagnostic wizard that tests all machine components in 8 phases: temperature sensors (5s), water pump (10s), heater (30s), valves (10s), the three containers C1/C2/C3 (10s each), and agitation motor (10s). The UI is split in two panels: a task list on the left showing icons per phase (check for done, dot for pending/skipped/stopped) and a detail panel on the right with phase description, real-time sensor data, countdown timer, and a progress bar. Three buttons control the flow: Stop (halts current phase), Start/Re-run (begins or repeats a phase), and Next (skips to the next phase). Each phase's state (done, skipped, stopped) is saved and visible when revisiting. When all phases complete successfully the title shows "Self-check complete!" in green; if any were skipped or stopped it shows "Self-check finished" in orange.
  • Load film — Loads the film onto the reel with the built-in loader: choose 135 or 120 and press Start. The reel motor winds the film while the Hall sensor counts the turns; when the reel stops at the end of the film, the servo raises the blade and cuts, then half a turn pulls the tail inside and a beep says the tank is ready. Cut now cuts at once (e.g. when the 120 tape shows), Stop halts everything, and with the machine idle a Test blade button runs one blade cycle. The cut is refused if the reel stops too early (clip not hooked) or turns too long, and the motor is checked for rotation right after the start. The same cycle can be started from the app; a process cannot start while a film is loading.
  • Import/Export — Backup and restore configuration to SD card (Export can also be run from the app; Import reboots the machine and stays on the display)
  • Statistics — Completed processes, total time, cleaning cycles, stopped processes
  • Software info — Firmware version (read from the running binary, also shown on the splash screen) and serial number
  • Update from SD — Firmware OTA update from a .bin file on the SD card. The system reads the firmware version from the binary header, asks for confirmation, then writes it to the secondary OTA partition. After completion, a reboot applies the new firmware. If the update fails, the bootloader automatically rolls back to the previous version.
  • Wi-Fi update — Starts a local web server on the board. A popup shows the board's IP address (e.g. http://192.168.1.42) and a randomly generated 8-digit PIN for security. The user opens the URL in any browser on the same network and sees a drag-and-drop upload page styled with the FilMachine branding. After uploading the .bin firmware file, it is streamed directly to the OTA partition. The board reboots automatically when complete. Wi-Fi credentials (SSID/password) are configured in the Settings tab.

Firmware Update (OTA)

The board supports two methods for over-the-air firmware updates. Both use the ESP-IDF dual-partition OTA mechanism: the new firmware is written to a secondary partition while the current one keeps running, and the bootloader swaps at reboot. If the new firmware fails to boot, automatic rollback restores the previous version.

Method 1: SD Card

  1. Build the firmware: idf.py build
  2. Copy build/FilMachine.bin to the SD card root, renamed as FilMachine_fw.bin
  3. Insert the SD card into the machine
  4. Go to Tools → "Update from SD" and press the play button
  5. Confirm the version shown in the popup
  6. Wait for the progress to reach 100%, then reboot

Method 2: Wi-Fi (Web Upload)

  1. Configure Wi-Fi SSID and password in Settings
  2. Go to Tools → "Wi-Fi update" and press the play button
  3. The popup shows the board's local IP and a 8-digit PIN
  4. On any device on the same network, open http://<board-ip> in a browser
  5. You'll see a drag-and-drop page — drop the .bin file or click to select it
  6. The firmware uploads in streaming and the board reboots automatically

Firmware File Format

The .bin file is produced by idf.py build at build/FilMachine.bin. It contains an ESP32 application image with an esp_app_desc_t header that stores the firmware version, build date, and SHA-256 hash for integrity verification.

Partition Table

The project uses a custom partition table (partitions.csv) optimized for the 16MB flash chip. Each OTA slot is 6MB, giving ~59% free space with the current firmware size (~2.5MB, measured on a clean build of v0.0.0.54 with the repository defaults).

text
# Name      Type  SubType   Offset      Size
nvs         data  nvs       0x9000      24KB     NVS key-value storage
otadata     data  ota       0xF000       8KB     OTA boot flag (which slot to boot)
phy_init    data  phy       0x11000      4KB     Wi-Fi PHY calibration data
ota_0       app   ota_0     0x20000      6MB     Firmware slot A (active)
ota_1       app   ota_1     0x620000     6MB     Firmware slot B (updated via OTA)
coredump    data  coredump  0xC20000    64KB     Crash snapshot (read into the SD boot log)
(free)      —     —         0xC30000    ~4MB     Unallocated

Note: after adding the coredump partition, regenerate the sdkconfig once (rm sdkconfig && idf.py build) so the CONFIG_ESP_COREDUMP_* defaults are picked up, then flash normally (idf.py flash also rewrites the partition table).

The bootloader alternates between ota_0 and ota_1: the new firmware is written to the inactive slot, and at reboot the bootloader swaps. If the new firmware crashes at startup, automatic rollback restores the previous working version.

Setting the Firmware Version

The single source of truth is version.txt in the project root, in the form MAJOR.MINOR.PATCH.BUILD (e.g. 0.0.0.54). ESP-IDF uses it as the application version, so the value shown on the splash and in Tools → Software version is read at runtime from the running binary via esp_app_get_description()->version; the simulator gets the same string from CMakeLists.txt, which injects it as FW_VERSION_STR.

./flash_p4.sh increments the BUILD field before every build. To do it by hand:

bash
./scripts/bump_version.sh          # 0.0.0.54 -> 0.0.0.55
./scripts/bump_version.sh --show   # print the current version without changing it

For a real release, edit version.txt directly (e.g. 0.1.0.0).


Hardware Specifications

System Architecture

FilMachine Hardware Architecture

Target Board

Guition JC4880P433 (-DBOARD_JC4880P433) — the only supported hardware board.

SpecDetail
MCUESP32-P4 (RISC-V dual-core @ 400 MHz)
Flash / PSRAM16 MB / 32 MB
DisplayST7701S 4.3" IPS, 480×800, MIPI-DSI (2-lane)
LVGL resolution800×480 (landscape, PPA-rotated)
TouchGT911 capacitive (I2C)
SD cardSDMMC 4-bit (~40 MB/s)
AudioES8311 codec + power amplifier
2D acceleratorPPA (rotate, scale, blend, fill)
SensorsFlow meter, water-bath level (min/max), 6× chemical level (3 containers × min/max), Hall effect, 2× DS18B20, chip internal temp
Wi-Fi / BLEESP32-C6 companion chip over SDIO (ESP-Hosted)
ESP-IDF version5.5.x+

Landscape Mode

The JC4880P433's physical display is 480×800 in portrait orientation. FilMachine uses it in landscape mode at 800×480. The display pipeline works as follows:

  1. LVGL renders the UI into an 800×480 framebuffer
  2. The PPA hardware engine rotates the buffer 90° to fill the 480×800 physical panel

Touch coordinates from the GT911 (which reports in 480×800 portrait) are inverse-mapped back to 800×480 LVGL coordinates. All of this is transparent to the UI code.

Expand IO Header (JP1) — JC4880P433 Pin Assignments

All external peripherals connect through the board's 2×13 Expand IO header (JP1):

JP1 PinGPIOFunction
1, 3—VCC 3.3V output (2 pins) — (power)
2, 4—VCC 5V output (2 pins) — (power)
5, 6, 16—GND (3 pins) — (power)
752Flow meter (YF-S201) — (used in code)
833Motor IN1, H-bridge ch.A direction A — (used in code)
951Pump ENA, LEDC PWM speed — (used in code)
1031Hall sensor (KY-003), reel turn counter for Load film — (used in code)
1150Pump IN2, H-bridge ch.B direction B — (used in code)
1230Water level max — (used in code)
1349Pump IN1, H-bridge ch.B direction A — (used in code)
1429Water level min (XKC-Y21) — (used in code)
1535DS18B20 OneWire, 2 sensors on shared bus — (used in code)
1734Motor IN2, H-bridge ch.A direction B — (used in code)
18—ESP_3V3 — (power, not used in code)
1932Motor ENA, LEDC PWM speed — (used in code)
20—C6_U0RXD (ESP32-C6) — (not used in code)
2128Cutter servo (MG90S), 50 Hz PWM — (used in code)
22—C6_U0TXD (ESP32-C6) — (not used in code)
237I2C SDA, shared bus: touch + MCP23017 — (used in code)
24—C6_IO9 (ESP32-C6) — (not used in code)
258I2C SCL, shared bus: touch + MCP23017 — (used in code)
26—C6_CHIP_PU (ESP32-C6) — (not used in code)

All 12 P4 GPIO pins on JP1 are allocated — GPIO 28, the last spare, now drives the film loader's cutter servo.

Breakout Board (KiCad)

hardware/FilMachine_Breakout_v1/ holds a KiCad project (schematic, PCB, the Python script that generates them, an HTML preview) and a BOM for a 70×50 mm two-layer breakout board. It plugs onto the Expand IO header (called J9 in its files) and brings the signals out to 5.08 mm screw terminals — motor, OneWire temperature, flow meter, I2C, water level, Hall sensor and power — plus a 1×8 header for the remaining GPIOs, and it carries the 4.7 kΩ pull-ups for I2C and OneWire. As its BOM.txt warns, check the header's power-pin mapping against the actual board with a multimeter before the first power-on.

Peripheral Drivers

ComponentDetail
Solenoid / IO expanderAdafruit I2C 8-Ch Solenoid Driver (#6318) — MCP23017 @ 0x20 on the shared I2C bus (GPIO 7/8). Port A drives the valves via on-board MOSFETs; Port B is broken out as raw GPIO and repurposed for level sensors + heater MOSFETs.
Motor DriverDBH-12V dual DC motor driver — channel A: agitation motor, channel B: pump
Temperature2× DS18B20 OneWire sensors (water bath + chemical) on one bus (GPIO 35, 4.7 kΩ pull-up)
PumpDC pump on DBH-12V ch.B — direction reversal for fill/drain (no pump valves)
Board powerUSB Type-C 5 V (logic) + external 12 V rail for solenoids/motor/pump/heaters (common ground)

MCP23017 pin map (I2C 0x20):

PinPortFunction
0A0Valve C1
1A1Valve C2
2A2Valve C3
3A3Valve WB (water bath)
4A4Valve WASTE
5A5Valve INLET (mains water into the bath, machine v6+)
6A6Valve RECOVERY (seventh port of the collector, hose to the recovery bottle — Mainboard SPARE OUT 1, machine v7)
7A7Bath circulator pump, 12 V (Mainboard SPARE OUT 2, machine v7) — on only while a process heats or develops
8B0Chem C1 level MIN (XKC-Y21)
9B1Chem C1 level MAX
10B2Chem C2 level MIN
11B3Chem C2 level MAX
12B4Chem C3 level MIN
13B5Chem C3 level MAX
14B6Heater MOSFET 1
15B7Heater MOSFET 2

Port A pins sit behind the #6318 MOSFET drivers (valves). Port B pins are raw push-pull GPIO on the board's bottom edge: B0–B5 are inputs (pull-up, LOW = water present), B6–B7 are heater outputs (HIGH = on).

Chemical Container Layout

LabelPurposeTypical Use
C1Chemical container 1Developer
C2Chemical container 2Bleach / Stop bath
C3Chemical container 3Fixer
WBWater bathRinse water (temperature-controlled)
WASTEWaste drainDiscarded chemicals

Constraints

ParameterLimit
Max processes50
Max steps per process30
Process/step name length20 characters
Minimum step duration30 seconds

Bill of Materials (Components)

Everything the firmware currently expects, for costing a build or a custom board. Quantities are for the standard 3-chemistry layout; prices are intentionally omitted (they move — estimate locally).

Two spreadsheets in the repository root complement this list (both in Italian): DistintaComponenti.xlsx, a parts list with specs, quantities, prices and links, and FilMachine_Custom_PCB_BOM.xlsx, the design worksheet for a future custom PCB (BOM, JP1 pin map, MCP23017 map, power budget). The MCP23017 map in that worksheet belongs to the custom-board design; the mapping the firmware uses today is the one in Peripheral Drivers.

Controller & storage

#ComponentQtyRole / notes
1Guition JC4880P433 board1ESP32-P4 + 4.3" ST7701S 480×800 display + GT911 touch + ES8311 audio + SDMMC + ESP32-C6 (Wi-Fi/BLE). Main controller + UI.
2microSD card1Config/process storage (FilMachine.json). Any small card.

Actuation & power drivers

#ComponentQtyRole / notes
3Adafruit #6318 I2C 8-ch solenoid driver (MCP23017 @0x20)1Port A → valves (MOSFET); Port B → level sensors + heater MOSFETs.
4Solenoid valve, 12 V5C1, C2, C3, WB, WASTE.
5DBH-12V dual DC motor driver (H-bridge)1Ch. A = agitation motor, ch. B = pump.
6DC gear motor, 12 V1Tank agitation / rotation.
7Kamoer KPHM600 peristaltic pump, 12 V (600 ml/min, PharMed tube 6.4 x 9.6)1Fill/drain by direction reversal; holds the liquid when stopped, pumps air to empty the line.
8Heater MOSFET module2Driven from MCP Port B (B6/B7).
9Heating element / immersion heater, 12 V1–2Water-bath heating (thermostat control).

Sensors

#ComponentQtyRole / notes
10DS18B20 waterproof probe (+ 4.7 kΩ pull-up)2Water bath + chemical temperature (one OneWire bus).
11XKC-Y21 non-contact level sensor8WB min+max (2) + 3 chem containers × min/max (6). Need 5 V + signal pulled to 3.3 V.
12Hall sensor (KY-003 / A3144)1Tank in-position / rotation; counts the reel turns while loading film.
13YF-S201 flow meter1Water-bath inlet flow.

Power & misc

#ComponentQtyRole / notes
1412 V PSU1Sized for valves + motor + pump + heaters (shared 12 V rail, common GND).
15USB-C 5 V1Board logic power.
16Resistors / level-shift, wiring, connectors—4.7 kΩ (DS18B20), pull-ups for XKC-Y21, common ground.
17Chemical containers + water bath + waste, tubing, enclosure—Mechanical build.

Future / diagnostics-ready (optional)

#ComponentQtyRole / notes
18Resistor dividers 12V/5V/3.3V → ADC13Enables live rail voltages in the diagnostics page (HAS_RAIL_MONITOR).
19INA219 current sensor (I2C)1+Current draw per rail/load (not yet wired in firmware).

Film Reel Auto-Loader (3D-Printed Prototype)

Bench prototype v1 — files in 3D_Models/FilMachine_caricatore_v1/. The original notes, in Italian, are in that folder's README.

The loader winds the film onto the reel from the centre outwards, like the Rondinax: a motor turns the reel, a leader strip with a clip pulls the film out of the cassette, a guide arm bows it and lays it onto the ribs, and a cutter trims it when the winding is done. Formats: 135 and 120 on the same reel.

Reel auto-loader, assembly for 135 film

What it is and what it is not. It is an open test bench for validating the mechanics in daylight with scrap film. It is not yet light-tight or liquid-tight: that is the next phase, to be designed around mechanics that work.

Firmware status. This v1 bench (stepper + pendulum cutter) is not driven by the firmware. The firmware drives the loader built into the machine (v5): Tools → Load film (main/film_loader.c) winds with the agitation motor, counts the reel turns with the Hall sensor, and cuts with a servo on GPIO 28 — see The Tools Tab. The sequence in Planned firmware sequence is kept as the reference for the bench.

Contents

FolderWhat's inside
step/21 parts as STEP, already in print orientation, plus the assemblies ASSIEME_135.step and ASSIEME_120.step
stl/the same 21 parts as STL
png/previews and section views
src/parametric model (Python + CadQuery): python3 build_all.py regenerates everything
verifiche.txtoutcome of the geometric checks from the last generation

Each assembly contains 23 named, coloured bodies: the 19 parts used for that format plus the REF_ reference shapes (NEMA17 motor, servo, blade, 135 cassette or 120 spool). In Shapr3D every body can be hidden. All part faces are planes, cylinders and cones, so they can be edited directly.

How It Works

  1. Adjustable reel (parts 01–04). Same principle as the OpenReel/Paterson reel, adapted to centre-out loading: a central tube with bayonet tracks, two flanges that slide on and lock with a 30° twist in two positions (35 mm and 120), and a central drum with the leader-strip anchor and the clip pocket. The flanges move symmetrically, so the film stays centred in both formats. Outer Ø 93.5 mm and tube bore Ø 26.3 mm, as measured on the OpenReel STEP files. The ribs have no outer lead-in and no ratchet: the film, under tension, rests with its edges on the back of the ribs (0.9 mm per side) and the emulsion touches nothing.
  2. Guide arm (05 for 135, 06 for 120). It pivots on two screws. Its converging walls squeeze the edges and bow the film into an arc (6.5 mm deep on 135, 8.7 mm on 120): that way it passes between the rib tips and, as soon as it leaves the tip of the arm, it flattens out again and settles on the right turn. The sole rests on the previous turn, so the arm rises by itself as the reel fills. With the reel empty, a stop screw on the cheek plate holds it.
  3. Turret (07–14). Two cheek plates, the bridge with the film slot and the blade slot, and the cradle for the 135 cassette or the one for the 120 roll (same four screws).
  4. Pendulum cutter (10–12). A micro servo swings an arm that carries an ordinary 9 mm snap-off cutter blade: the tip crosses the slot from one side to the other and cuts the film against the ceiling of the bridge, like a sliding trimmer. It cuts the full width, 120 included.
  5. Drive (15–19). NEMA17 on one upright, a single drive coupling for both formats (a spigot in the tube bore plus two teeth), a plug-type idler pin on the other upright, and the base.
  6. Clip (20–21). A wedge jaw with a tooth, closed by a slider; it stays in the drum pocket.

Adjustable reel, exploded view Pendulum cutter at three positions of its sweep

Left: the adjustable reel, exploded (flanges, bayonet tube with the drum). Right: the pendulum cutter at three positions of its sweep.

Printing

PETG, 0.4 mm nozzle. No supports: every part is already oriented.

PartsNotes
01, 02 flanges0.12–0.16 mm layers; the ribs bridge across the windows (6 mm)
03 tubeupright; the bayonet V-grooves are at 45°, 0.12–0.16 mm layers
05, 06 guide armsupside down (roof on the bed); 0.6 mm walls at the tip: enable thin walls / Arachne
09 bridgestanding on its short side, with a brim
20 clip jawon its side, 0.12 mm layers; it is the finest part
all the others0.2 mm layers, 3 perimeters, 15–20 % infill

Fit clearance used everywhere: 0.15 mm (FIT in src/common.py). If your printer prints tight or loose, change that value and regenerate.

Parts to Source

  • NEMA17 stepper motor (Ø5 × 24 mm shaft) + TMC2209 driver
  • MG90S micro servo (or SG90) with the single-arm horn
  • 9 mm snap-off cutter blade (0.4 mm), a piece at least 45 mm long
  • M3 screws: 12 × M3×12 (bridge, cradle, servo mount), 9 × M3×10 (turret feet, upright feet, idler-pin retainer), 10 × M3×8 (motor, guide-arm pivot and stop, blade clamp), 1 × M3×6 grub screw + 1 M3 nut
  • 1.75 mm filament: it serves as the leader-strip pin (drum and clip) and as the drum-to-tube dowel (2 pieces of 6 mm)
  • Leader strip: a 16 × ~135 mm strip of scrap film or 0.15–0.2 mm PET (115 mm usable)

The holes marked "self-tapping" are Ø2.6: the M3 screw cuts its own thread in the PETG.

Assembly

  1. Tube + drum: slide the drum to the middle of the tube and fix it with the two filament dowels.
  2. Leader strip: one end around a piece of filament pushed into the hole of the drum, the other end in the head of the clip.
  3. Flanges: slide them onto the tube with the teeth in the channels, push to the wanted position (35 or 120), then twist 30° up to the stop. Flange A goes on the motor side.
  4. Turret: screw the bridge, the cradle and the servo mount between the two cheek plates, then fit the guide arm with its two pivot screws (they must leave it free to swing) and the stop screw.
  5. Cutter: glue the servo horn into its seat in the blade arm, then mount the blade with the edge towards the cutting side and the tip 75 mm from the servo axis (19 mm beyond the head of the arm).
  6. Uprights and base; drive coupling on the motor shaft, held by the grub screw.

Use

135 — Cassette in the cradle with the lips up, towards the bridge; leader trimmed square. Arm raised. Bring the clip to the cassette, passing under the arm and through the central passage of the bridge, clip it onto the film (1 cm sticking out is enough), lower the arm. Start: the motor winds, stops at the end of the film, the blade cuts, and the motor takes up the tail.

120 — Flanges in the 120 position, arm 06, cradle 14. Roll in the cradle; unroll the backing paper until the film appears, then clip onto the film. While the motor winds, guide the paper up and back (as on the Rondinax 60). When the adhesive tape arrives: stop, advance 20 mm, cut.

Planned Firmware Sequence (Starting Values)

Reel turns: 0.83 of leader strip, then 8.2 turns for a 36-exposure 135 (5.9 for 24 exposures) and 4.6 turns for a 120. Length wound after θ radians: L = 23·θ + 0.175·θ² mm.

  1. Servo parked (−26.8° from vertical).
  2. Wind at 15–20 rpm, clockwise as seen from the idler-pin side.
  3. End of film on 135: motor stall (StallGuard) at low current, or a maximum number of turns.
  4. Cut: servo from −26.8° to +26.5° and back.
  5. Take up the tail: another 0.6 turns.

Motor current, stall threshold and servo pulse widths have to be tuned on the bench: they have not been measured.

Checks Done on the Model

See verifiche.txt for the full output.

  • All 21 parts are valid, single solids; neither assembly has any pair of interfering bodies.
  • Bayonet: free insertion along the channel, free rotation up to the stop, locked beyond the stop and against pull-out, in both positions.
  • Guide arm: no interference with the reel at three winding radii, 0.2 mm clearance per side.
  • Cutter: sweep free of interference in 7 positions; the tip passes the ceiling of the slot by 1.1 mm at the edges of the 120; when parked, the blade stays 0.9 mm from the 120 film zone and 12.8 mm from the 135 one.
  • Reel capacity: 1.83 m of film.

To Be Validated on the Bench

These are not guaranteed by CAD alone.

  1. Forming the arc and laying the film on the ribs: this is the heart of the system. Parameters to retouch if needed: RIB_H, TIP_CLEAR, WALL_TIP, H_EDGE.
  2. Clip grip under tension. For the first trials a piece of adhesive tape works too.
  3. Bayonet without a detent: while winding, friction keeps it against the stop; for agitation with reversals a positive lock will be needed (a key in the channel).
  4. Cutting force: estimated, not measured. If the MG90S is not enough, the mount has to be adapted to a larger servo.
  5. End of film: stall detection to be tuned on 135; on 120 it is manual in this version, like pulling out the backing paper.
  6. Fit in the developing tank: the tube is 71 mm long even in the 35 mm configuration.

Modifying the Model

All dimensions are in src/common.py. After a change, run cd src && python3 build_all.py (needs pip install cadquery): it re-exports parts and assemblies and re-runs all the checks.


Diagnostics & Bring-up Page

A hidden factory/bring-up screen for validating hardware (especially a custom board).

How to open:

  • Board: on the splash, hold 2 fingers still for 3 seconds. Touch after the splash appears — a capacitive GT911 calibrates out fingers already on the panel at power-on.
  • Simulator: press the small DBG button (top-left of the splash) — the mouse can't do multi-touch.

Live monitor (grouped panels, refresh ~3×/s): bath/chem temperatures, water-bath level (min/max), 3× chemical level (min/max), Hall magnet, flow (L/min + pulses), MCP23017 / SD / Wi-Fi status, and a System panel with ESP32-P4 internal chip temperature, free PSRAM, last reset reason, and uptime. A Power rails panel shows 12V/5V/3.3V (n/a until dividers + HAS_RAIL_MONITOR are provided).

Output controls (to test the board): toggle each valve (C1/C2/C3/WB/Waste/Rec — the recovery outlet on A6), run pump FW/RV/Stop, motor FW/RV/Stop, toggle heaters H1/H2 and the bath circulator (Circ, A7), play an audio test tone. Close turns every output off and returns to the splash.


Build-time Switches

Compile-time flags (in main/include/FilMachine.h and the board header) that change behaviour without touching logic:

FlagDefaultEffect
ENABLE_BOOT_ERRORS01 → show a blocking SD/I2C error screen at boot (tap to reboot). At 0 the errors are still detected + logged.
VALVE_SELFTEST_ON_BOOT1Cycle the valves once at boot (runs in the background so the display isn't delayed).
HAS_DUAL_HEATER1Two heater MOSFETs on MCP B6/B7 (vs a single Port-A relay).
HAS_CHEM_LEVEL_SENSORS16 chemical-level sensors on MCP Port B (B0–B5).
HAS_RAIL_MONITOR01 → read 12V/5V/3.3V rails via ADC in the diagnostics page (needs dividers + channel/ratio defines).
MOTOR_MIN_ANALOG_VAL / PUMP_MIN_ANALOG_VAL90 / 64Lowest duty applied (motor: at the 10% slider position; pump: floor of the linear 0..250 mapping) — raise if the motor/pump stalls at low speed.
PUMP_FLOW_ML_MIN (board header)600Flow of the pump at 100 %: fill/drain times of the process and of the Tools are computed from it (ml / flow).
PUMP_PURGE_SECS4Seconds of air pumped after every drain (at 100 %, scaled with the speed): the tank is empty, so the liquid still in the line is pushed to where the drain went and the next fill starts with a dry line.
XFER_CYCLE_ML300Volume of one tank cycle when the Tools move liquid between two collector ports (the tank is the measuring cup; it overflows at ~500 ml).
CHEM_CONTAINER_ML / BATH_VOLUME_ML1000 / 12800Nominal volumes for the Tools' time estimates until a measured fill time exists.

Configuration & Persistence

All data is stored on the SD card as a single JSON file, FilMachine.json (the old binary .cfg was replaced — JSON is human-readable, hand-editable and version-tolerant). Parsing/serialisation uses a small dependency-free parser (main/mini_json.c) that builds on both the board and the simulator (no cJSON). Writes are crash-safe (.tmp + atomic rename).

Schema:

json
{
  "settingsParams": {
    "tempUnit": 0, "waterInlet": 1,
    "filmRotationSpeedSetpoint": 50, "rotationIntervalSetpoint": 10, "randomSetpoint": 20,
    "isPersistentAlarm": 1, "isProcessAutostart": 0, "drainFillOverlapSetpoint": 100,
    "multiRinseTime": 60, "lineRinseEnabled": 0, "lineRinseTime": 10,
    "tankSize": 2, "pumpSpeed": 100,
    "chemCalibFillSecs": 0, "wbCalibFillSecs": 0, "chemistryVolume": 2,
    "tempCalibOffset": 0, "chemCalibOffset": 0,
    "splashRandom": 0, "splashPalette": 3, "splashShapeStyle": 0,
    "splashComplexity": 40, "splashSeed": 0, "splashDefault": 1,
    "wifiEnabled": 0, "wifiSSID": "", "wifiPassword": "",
    "brightness": 100, "volume": 60, "invertPump": 0,
    "language": 0, "screenOffMins": 10
  },
  "processes": [
    { "name": "C41 Color Std", "temp": 38, "tempTolerance": 0.3,
      "isTempControlled": 1, "isPreferred": 1, "filmType": 1,
      "timeMins": 23, "timeSecs": 15,
      "steps": [
        { "stepNameString": "Developer", "timeMins": 3, "timeSecs": 15,
          "type": 0, "source": 0, "discardAfterProc": 0 }
      ] }
  ],
  "machineStats": { "completed": 0, "totalMins": 0, "totalSecs": 0, "stopped": 0, "clean": 0 }
}

Notes on a few fields: tempCalibOffset/chemCalibOffset are in tenths of a degree (int16, clamped ±30 °C); chemCalibFillSecs/wbCalibFillSecs are the measured MIN→MAX fill times in seconds (0 = uncalibrated, capacity used as fallback); step type 0=Chemistry/1=Rinse/2=MultiRinse; step source 0=C1,1=C2,2=C3,3=WB. lineRinseEnabled (0/1) turns on a line rinse after each chemistry step: after draining a chemistry step, the shared pump line is flushed with clean water from the water bath, discarded to waste, for lineRinseTime seconds (5–60 s) before the next liquid is drawn — clearing chemistry residue from the common tubing to avoid cross-contamination. Opt-in (default off); uses extra water, so watch the bath level when the water inlet isn't connected.

Auto-save triggers: creating/editing/deleting a process, changing any setting, toggling preferred, duplicating processes or steps, and recording a fill calibration.

Export writes a backup copy (FilMachine_Backup.json); Import restores from it and reboots. Generate a fresh SD file with scripts/genFilMachineCFG.py --realistic --output sd/ (see COMMANDS.md).

In the simulator these files live in an sd/ subdirectory of the directory the simulator is launched from (e.g. sd/FilMachine.json); the folder is created on first run if missing.


Language / Localization

The UI is fully bilingual: English (default) and Italiano. The language is selected in Settings → Language via a roller; on confirmation the machine saves the config and reboots automatically to rebuild the UI in the new language (the on-device UI is built once at boot).

Implementation: all ~350 UI strings live in a runtime lookup table (main/lang.c + main/include/lang.h) resolved through tr(). The xxx_text macros in FilMachine.h are untouched at call sites — they now expand to tr(STR_xxx). The table is generated/maintained by scripts/gen_lang.py, which holds the Italian dictionary; translations use ASCII apostrophes (Velocita') because the LVGL fonts are converted with --range 0x20-0x7F. The setting is persisted as "language" (0=EN, 1=IT) in FilMachine.json, exposed over WebSocket (set_setting, applied at next boot), and included in genFilMachineCFG.py.

To add or fix translations, edit the IT dict in scripts/gen_lang.py and mirror the change in main/lang.c (or regenerate). Adding a third language means extending lang_pair_t and the languageList roller options.


SD Boot & Crash Log

Every boot the firmware creates /log/boot_NNNN.txt on the SD card (the 20 most recent files are kept, older ones are pruned automatically). The file starts with a header — firmware version, build date, reset reason — and then captures in background all ESP_LOGx and LV_LOG lines (RAM-buffered, flushed to the card every 2 s so the UI is never blocked).

If the previous run crashed (panic or watchdog), the header also contains a PREVIOUS RUN CRASHED block with the offending task and program counter, read back from the core dump that the ESP32 saves to the dedicated 64KB coredump flash partition at panic time. The full dump stays in flash and can be decoded with idf.py coredump-info. This is the "black box": if the machine reboots on its own while no serial console is attached, pull the SD card and read the latest boot_NNNN.txt.

Not captured by design: bootloader output, the very first lines before the SD is mounted, raw printf calls, and the last ≲2 s before a hard crash (covered by the core dump summary instead). Module: main/sd_log.c (firmware only; the simulator links weak no-op stubs).


Typical Workflows

C41 Color Negative (38°C)

A standard C41 process: Pre-wash with water (1:00), Developer from C1 (3:15), Bleach from C2 (6:30), Wash (3:00), Fixer from C3 (6:30), Final Wash (3:00). Temperature control is critical — the machine maintains 38°C ± 0.3°C throughout.

Black & White (20°C)

B&W processes are more flexible: Developer from C1 (8:00), Stop Bath from C2 (1:00), Fixer from C3 (5:00), Wash (5:00). Temperature control can be disabled for ambient-temperature development, or set to 20°C ± 0.5°C for consistency.

E6 Slide Film (38°C)

E6 requires precise timing and temperature: First Developer (6:00), Wash (2:00), Color Developer (6:00), Wash (2:00), Bleach (6:00), Fixer (4:00), Final Wash (4:00). All at 38°C with tight ±0.3°C tolerance.


Developer Quick Reference

Build Commands

bash
# ── Simulator (macOS/Linux) ──────────────────────────────
mkdir -p build800 && cd build800
cmake ..                                   # Configure (first time only)
make -j$(sysctl -n hw.ncpu) filmachine_sim # Build simulator (parallel)
./filmachine_sim                           # Run simulator (800×480 window)

# ── Tests ────────────────────────────────────────────────
make -j$(sysctl -n hw.ncpu) filmachine_test  # Build tests (parallel)
./filmachine_test                            # Run all 19 test suites

# ── Firmware (ESP32-P4) ──────────────────────────────────
cd ~/Documents/GitHub/FilMachine_Reworked       # Must be in project root!
. $HOME/esp/esp-idf-v5.5/export.sh        # Load ESP-IDF environment
idf.py set-target esp32p4                  # First time: set RISC-V target
idf.py build                               # Build firmware
idf.py flash monitor                       # Flash + open serial monitor
idf.py flash                               # Flash only (no monitor)
idf.py monitor                             # Serial monitor only (Ctrl+] to exit)
idf.py size                                # Show firmware size breakdown

Rebuilding After Code Changes

bash
# Simulator: just run make again (incremental, from build800/)
cd build800 && make -j$(sysctl -n hw.ncpu) filmachine_sim

# If you added new .c files to CMakeLists.txt, re-run cmake first:
cd build800 && cmake .. && make -j$(sysctl -n hw.ncpu) filmachine_sim

# Build + run tests in one shot:
cd build800 && cmake .. && make -j$(sysctl -n hw.ncpu) filmachine_test && ./filmachine_test

# Firmware: idf.py handles incremental builds automatically
idf.py build

# Flash immediately after build:
idf.py build && idf.py flash monitor

Clean Rebuild

bash
# Simulator: wipe build directory and reconfigure
rm -rf build800 && mkdir build800 && cd build800 && cmake ..
make -j$(sysctl -n hw.ncpu) filmachine_sim

# Firmware: full clean (also needed if sdkconfig changes)
idf.py fullclean
idf.py build

Generating a Firmware Update File

bash
idf.py build
# The .bin file is at: build/FilMachine.bin
# For SD card update, rename and copy:
cp build/FilMachine.bin /Volumes/SDCARD/FilMachine_fw.bin

Generating Sample Data

bash
python3 scripts/genFilMachineCFG.py --realistic --output build800/sd/

Common Troubleshooting

bash
# "CMakeLists.txt not found" when running idf.py
# → You're in the wrong directory. Run from the project root, not build800/

# Simulator won't compile after adding a new .c file
# → Re-run cmake: cd build800 && cmake ..

# LVGL not found
# → CMake clones it on the first configure. To do it by hand, from the project root:
#   git clone --depth 1 --branch v9.5.0 https://github.com/lvgl/lvgl.git

# Serial monitor garbled output
# → Check baud rate: idf.py monitor -b 115200

# ESP-IDF not found / idf.py command not found
# → Source the environment: . $HOME/esp/esp-idf-v5.5/export.sh

# CMake: 'The "path" field in the manifest file ".../components/espressif__esp_lcd_touch/idf_component.yml"
#         does not point to a directory'
# → dependencies.lock stores the absolute path of that local component on the machine that
#   generated it. After moving or renaming the project folder (or on another computer), edit the
#   "path:" under espressif/esp_lcd_touch in dependencies.lock to the new location. Deleting the
#   lock file makes the component manager regenerate it, but with newer versions of the unpinned
#   dependencies (mdns, esp_lcd_touch_gt911 and two transitive ones), which are untested here.

License

Private project. All rights reserved.

Authors

PeterB — Hardware design & firmware FrankP — Software development & testing

Parts

205
QtyValueFootprintReferences
19100kResistor_SMD:R_0603_1608MetricR1, R41, R42, R43, R44, R45, R46, R47, R48, R61, R62, R70, R71, R72, R73, R74 +3
164.7kResistor_SMD:R_0603_1608MetricR3, R6, R7, R16, R51, R52, R53, R54, R55, R56, R57, R58, R67, R68, R80, R82
121N5819WSDiode_SMD:D_SOD-323D6, D7, D10, D11, D12, D13, D47, D48, D49, D50, D51, D52
11100nFCapacitor_SMD:C_0603_1608MetricC5, C13, C14, C15, C16, C17, C18, C19, C41, C50, C51
1010kResistor_SMD:R_0603_1608MetricR8, R12, R13, R14, R20, R21, R22, R23, R64, R66
91kResistor_SMD:R_0603_1608MetricR4, R5, R84, R97, R98, R99, R100, R101, R102
8100RResistor_SMD:R_0603_1608MetricR31, R32, R33, R34, R35, R36, R37, R38
8AO3400APackage_TO_SOT_SMD:SOT-23Q1, Q2, Q3, Q4, Q5, Q6, Q7, Q8
8B5819WDiode_SMD:D_SOD-123D21, D22, D23, D24, D25, D26, D27, D28
8YellowLED_SMD:LED_0805_2012MetricD31, D32, D33, D34, D35, D36, D37, D38
61nFCapacitor_SMD:C_0603_1608MetricC20, C21, C22, C23, C54, C61
522uF 25VCapacitor_SMD:C_1206_3216MetricC7, C8, C9, C10, C59
4M3MountingHole:MountingHole_3.2mm_M3H1, H2, H3, H4
3100nF 50VCapacitor_SMD:C_0805_2012MetricC52, C53, C58
310uF 25VCapacitor_SMD:C_0805_2012MetricC62, C63, C64
310uF 50VCapacitor_SMD:C_1206_3216MetricC2, C3, C57
315AFM:FUSE_2410_SMDF1, F2, F3
3GreenLED_SMD:LED_0805_2012MetricD3, D4, D5
3SS34Diode_SMD:D_SMAD8, D41, D42
21.1AFuse:Fuse_1812_4532MetricF5, F7
21.5kResistor_SMD:R_0603_1608MetricR85, R88
210RResistor_SMD:R_0603_1608MetricR63, R65
2470uF 25VCapacitor_THT:CP_Radial_D10.0mm_P5.00mmC55, C56
274LVC2G08Package_SO:SSOP-8_2.95x2.8mm_P0.65mmU6, U7
2AOD4184APackage_TO_SOT_SMD:TO-252-2Q9, Q10
2BTN8962TAPackage_TO_SOT_SMD:TO-263-7_TabPin8U8, U9
2RedLED_SMD:LED_0603_1608MetricD43, D44
2SMBJ16ADiode_SMD:D_SMBD1, D2
10.5AFuse:Fuse_1206_3216MetricF6
1100uF 35VCapacitor_THT:CP_Radial_D6.3mm_P2.50mmC1
110uHFM:L_SLO0630HL1
112V IN HEATER 1FM:TB_WJ500V-5.08-2PJ1
112V IN HEATER 2FM:TB_WJ500V-5.08-2PJ2
112V IN MAINFM:TB_WJ500V-5.08-2PJ3
112V OUTFM:TB_DB301V-3.5-2PJ28
115kResistor_SMD:R_0603_1608MetricR10
11uF 50VCapacitor_SMD:C_0603_1608MetricC6
11uF 50VCapacitor_SMD:C_0805_2012MetricC40
120kResistor_SMD:R_0603_1608MetricR11
1220RResistor_SMD:R_0603_1608MetricR87
1220nF 50VCapacitor_SMD:C_0805_2012MetricC4
124kResistor_SMD:R_0603_1608MetricR2
12AFuse:Fuse_1812_4532MetricF8
130kResistor_SMD:R_0603_1608MetricR86
147uF 10VCapacitor_SMD:C_1206_3216MetricC60
15V OUTFM:TB_DB301V-3.5-2PJ29
18AFM:FUSE_2410_SMDF4
1ADS1115Package_SO:VSSOP-10_3x3mm_P0.5mmU4
1BATH LEVELConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ7
1C6 UARTConnector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalJ6
1DISPLAY JP1Connector_IDC:IDC-Header_2x13_P2.54mm_HorizontalJ5
1DRV8871DDAPackage_SO:TI_SO-PowerPAD-8_ThermalViasU10
1FLOWConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ11
1GPIO SPAREConnector_PinHeader_2.54mm:PinHeader_1x09_P2.54mm_VerticalJ30
1HALLConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ12
1HEATER 1FM:TB_WJ500V-5.08-2PJ23
1HEATER 2FM:TB_WJ500V-5.08-2PJ24
1LMR33630ADDAPackage_SO:TI_SO-PowerPAD-8_ThermalViasU1
1MCP23017-E/SSPackage_SO:SSOP-28_5.3x10.2mm_P0.65mmU3
1MOTORFM:TB_DB301V-3.5-2PJ26
1PUMPFM:TB_WJ500V-5.08-2PJ25
1SERVOConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalJ27
1SPARE OUT 1FM:TB_DB301V-3.5-2PJ21
1SPARE OUT 2FM:TB_DB301V-3.5-2PJ22
1TEMP 1Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ13
1TEMP 2Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ14
1UCC27524DPackage_SO:SOIC-8_3.9x4.9mm_P1.27mmU5
1VALVE C1FM:TB_DB301V-3.5-2PJ15
1VALVE C2FM:TB_DB301V-3.5-2PJ16
1VALVE C3FM:TB_DB301V-3.5-2PJ17
1VALVE INLETFM:TB_DB301V-3.5-2PJ20
1VALVE WASTEFM:TB_DB301V-3.5-2PJ19
1VALVE WBFM:TB_DB301V-3.5-2PJ18

Similar boards

  • 2 layers155.5 × 65.9 mmPicozorro one th, the printed circuit board in 3D

    Picozorro one th

    maximevince/PicoZorro

    Amiga Zorro-II Bus Cards using nothing but an RPi Pico to interact with the bus

    48 parts2
  • 2 layers80 × 55 mmIm01, the printed circuit board in 3D

    Im01

    amuzdgp3-prog/im-01-pulse-device

    Отдельный модуль к системе обслуживания аппаратов 2.apixspb.ru. Устройство слушает импульсные выходы монетоприёмника и купюроприёмника, копит показания в FRAM и дважды в сутки передаёт их на сервер по LTE Cat.1.

    108 parts
  • 4 layers100 × 30 mmRevo main, the printed circuit board in 3D

    Revo main

    ratmuds/revo

    a "cheap" 6 DoF robot arm intentionally pushing the limits of low quality hobby servos

    58 parts2MIT
  • 2 layersMain Platine, the printed circuit board in 3D

    Main Platine

    Mistbiene0815/Mistbiene0815-2_vs_2_Lightweight_Platinen

    Unsere Platinen für den 2 vs 2 bot

    27 parts
  • 2 layers148.7 × 180 mmMainPlatine, the printed circuit board in 3D

    MainPlatine

    rstiebel/Bohlebots_Atlantis

    Bohlebots Atlantis files

    38 parts2
  • 2 layers140 × 50 mmYahrzeit power dist, the printed circuit board in 3D

    Yahrzeit power dist

    allanschwartz/yahrzeit

    Yahrzeit Wall server and embedded code

    27 parts
  • 4 layers260 × 113 mmCellKeeper, the printed circuit board in 3D

    CellKeeper

    cellkeeper-bms/cellkeeper-hardware

    PCB for the CellKeeper BMS

    480 parts11GPL-3.0
  • 4 layers159.6 × 130 mmFE UFPR 4 0, the printed circuit board in 3D

    FE UFPR 4 0

    josearistidesbail/FE_UFPR_4_0

    KiCad 10 project for the board that sits between a TI LAUNCHXL-F28379D (runs the FOC firmware) and an Infineon PrimeSTACK 6PS04512E43W39693 inverter driving an EMRAX 208 motor.

    374 parts

Questions

How big is the FilMachine Mainboard PCB?

It measures 110 × 90 mm and has 4 copper layers, 1.6 mm thick.

What parts does the FilMachine Mainboard use?

205 parts, built around the BTN8962TAAUMA1, ADS1115IDGSR, DRV8871DDARQ1 and LMR33630ADDA. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the FilMachine Mainboard KiCad files?

From the yesnoj/FilMachine_Reworked repository on GitHub, at the commit this page was made from: the board is PCB/kicad/FilMachine_Mainboard.kicad_pcb and its schematic PCB/kicad/FilMachine_Mainboard.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the FilMachine Mainboard design in my own project?

Its repository states no licence, so its author keeps all rights: ask yesnoj before reusing the design.