Skip to content

Commit 3f83640

Browse files
Add gaming mode features with edge switching support
1 parent 9151e95 commit 3f83640

File tree

14 files changed

+344
-17
lines changed

14 files changed

+344
-17
lines changed

disk/create_disk.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import tempfile
4+
import fs
5+
from pyfatfs.PyFat import PyFat
6+
7+
def create_fat12_image(output_path: str, input_file_path: str) -> None:
8+
"""
9+
Creates a 64 KB FAT12 image identical to the original mkdosfs + dd workflow.
10+
No sudo required.
11+
"""
12+
image_size_for_calc = 2 * 1024 * 1024 # 2 MB — needed for correct geometry
13+
final_image_size = 64 * 1024 # 64 KB — what DeskHop embeds
14+
volume_label = "DESKHOP"
15+
16+
# Use a named temporary file so pyfatfs can open it by path
17+
temp_file = tempfile.NamedTemporaryFile(delete=False)
18+
temp_path = temp_file.name
19+
temp_file.close()
20+
21+
try:
22+
# 1. Zero-fill the 2 MB temporary image
23+
with open(temp_path, "wb") as f:
24+
f.write(bytes(image_size_for_calc))
25+
26+
# 2. Format as FAT12 using pyfatfs low-level API
27+
formatter = PyFat(encoding="utf-8")
28+
formatter.mkfs(
29+
temp_path,
30+
fat_type=12,
31+
size=image_size_for_calc,
32+
label=volume_label.ljust(11)[:11] # Ensure exactly 11 chars
33+
)
34+
formatter.close()
35+
36+
# 3. Mount with high-level fs.py and copy config.htm
37+
fat_url = f"fat://{temp_path}"
38+
with fs.open_fs(fat_url) as fat_fs:
39+
with open(input_file_path, "rb") as src:
40+
fat_fs.writebytes("config.htm", src.read())
41+
42+
# 4. Truncate to final 64 KB
43+
with open(temp_path, "rb") as src:
44+
data = src.read(final_image_size)
45+
46+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
47+
with open(output_path, "wb") as dst:
48+
dst.write(data)
49+
50+
finally:
51+
# Always remove temp file, even on KeyboardInterrupt
52+
try:
53+
os.unlink(temp_path)
54+
except OSError:
55+
pass
56+
57+
58+
if __name__ == "__main__":
59+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
60+
output_file = os.path.join(project_root, "disk", "disk.img")
61+
input_file = os.path.join(project_root, "webconfig", "config.htm")
62+
63+
if not os.path.isfile(input_file):
64+
raise FileNotFoundError(f"Config file not found: {input_file}")
65+
66+
create_fat12_image(output_file, input_file)
67+
print(f"disk.img created successfully → {output_file}")

disk/disk.img

0 Bytes
Binary file not shown.

disk/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pyfatfs>=0.5.0
2+
fs>=2.4.16

misc/generate_disk.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
3+
# Exit immediately if a command exits with a non-zero status.
4+
set -e
5+
6+
# Get the directory of this script so we can use relative paths
7+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
8+
PROJECT_ROOT="$SCRIPT_DIR/.."
9+
WEBCONFIG_DIR="$PROJECT_ROOT/webconfig"
10+
DISK_DIR="$PROJECT_ROOT/disk"
11+
12+
# --- 0. Clean previous virtual environments ---
13+
echo "--- Cleaning up old virtual environments ---"
14+
rm -rf "$WEBCONFIG_DIR/venv"
15+
rm -rf "$DISK_DIR/venv"
16+
echo "Cleanup complete."
17+
echo ""
18+
19+
# --- 1. Render the web configuration page ---
20+
echo "--- Running Webconfig Renderer ---"
21+
pushd "$WEBCONFIG_DIR" > /dev/null
22+
23+
VENV_DIR="venv"
24+
echo "Creating new virtual environment..."
25+
python3 -m venv "$VENV_DIR"
26+
source "$VENV_DIR/bin/activate"
27+
pip install -r requirements.txt
28+
29+
python3 -B render.py
30+
deactivate
31+
echo "Render complete."
32+
popd > /dev/null
33+
echo ""
34+
35+
# --- 2. Generate the disk image ---
36+
echo "--- Generating Disk Image ---"
37+
pushd "$DISK_DIR" > /dev/null
38+
39+
VENV_DIR="venv"
40+
echo "Creating new virtual environment..."
41+
python3 -m venv "$VENV_DIR"
42+
source "$VENV_DIR/bin/activate"
43+
pip install -r requirements.txt
44+
45+
python3 create_disk.py
46+
deactivate
47+
echo "Disk image generation complete."
48+
popd > /dev/null

misc/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker-compose -f docker.yml run --rm build_container sh -c "rm -rf build && cmake -S . -B build -DDH_DEBUG=ON && cmake --build build"

src/defaults.c

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,21 @@
1414
const config_t default_config = {
1515
.magic_header = 0xB00B1E5,
1616
.version = CURRENT_CONFIG_VERSION,
17-
.output[OUTPUT_A] =
18-
{
17+
.enforce_ports = ENFORCE_PORTS,
18+
.force_kbd_boot_protocol = ENFORCE_KEYBOARD_BOOT_PROTOCOL,
19+
.force_mouse_boot_mode = false,
20+
.enable_acceleration = ENABLE_ACCELERATION,
21+
.hotkey_toggle = HOTKEY_TOGGLE,
22+
.kbd_led_as_indicator = KBD_LED_AS_INDICATOR,
23+
.jump_threshold = JUMP_THRESHOLD,
24+
25+
.gaming_mode_on_boot = true,
26+
.gaming_edge_enabled = false,
27+
.gaming_edge_threshold = 500,
28+
.gaming_edge_window_ms = 500,
29+
30+
.output = {
31+
[OUTPUT_A] = {
1932
.number = OUTPUT_A,
2033
.speed_x = MOUSE_SPEED_A_FACTOR_X,
2134
.speed_y = MOUSE_SPEED_A_FACTOR_Y,
@@ -34,8 +47,7 @@ const config_t default_config = {
3447
.max_time_us = (uint64_t)SCREENSAVER_A_MAX_TIME_SEC * 1000000,
3548
}
3649
},
37-
.output[OUTPUT_B] =
38-
{
50+
[OUTPUT_B] = {
3951
.number = OUTPUT_B,
4052
.speed_x = MOUSE_SPEED_B_FACTOR_X,
4153
.speed_y = MOUSE_SPEED_B_FACTOR_Y,
@@ -53,12 +65,6 @@ const config_t default_config = {
5365
.idle_time_us = (uint64_t)SCREENSAVER_B_IDLE_TIME_SEC * 1000000,
5466
.max_time_us = (uint64_t)SCREENSAVER_B_MAX_TIME_SEC * 1000000,
5567
}
56-
},
57-
.enforce_ports = ENFORCE_PORTS,
58-
.force_kbd_boot_protocol = ENFORCE_KEYBOARD_BOOT_PROTOCOL,
59-
.force_mouse_boot_mode = false,
60-
.enable_acceleration = ENABLE_ACCELERATION,
61-
.hotkey_toggle = HOTKEY_TOGGLE,
62-
.kbd_led_as_indicator = KBD_LED_AS_INDICATOR,
63-
.jump_threshold = JUMP_THRESHOLD,
68+
}
69+
}
6470
};

src/include/config.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#include "misc.h"
1616
#include "screen.h"
1717

18-
#define CURRENT_CONFIG_VERSION 8
18+
#define CURRENT_CONFIG_VERSION 10
1919

2020
/*==============================================================================
2121
* Configuration Data

src/include/structs.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ typedef struct {
7979
uint8_t enforce_ports;
8080
uint16_t jump_threshold;
8181

82+
uint8_t gaming_mode_on_boot;
83+
84+
uint8_t gaming_edge_enabled;
85+
uint16_t gaming_edge_threshold;
86+
uint16_t gaming_edge_window_ms;
87+
8288
output_t output[NUM_SCREENS];
8389
uint32_t _reserved;
8490

@@ -144,6 +150,10 @@ typedef struct {
144150
bool config_mode_active; // True when config mode is active
145151
bool digitizer_active; // True when digitizer Win/Mac workaround is active
146152

153+
/* Gaming mode edge switching state */
154+
int32_t gaming_edge_accum; // Accumulated movement toward edge in gaming mode
155+
uint64_t gaming_edge_last_reset; // Timestamp of last accumulator reset
156+
147157
/* Onboard LED blinky (provide feedback when e.g. mouse connected) */
148158
int32_t blinks_left; // How many blink transitions are left
149159
int32_t last_led_change; // Timestamp of the last time led state transitioned

src/mouse.c

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,12 @@ void switch_virtual_desktop(device_t *state, output_t *output, int new_index, in
240240
void do_screen_switch(device_t *state, int direction) {
241241
output_t *output = &state->config.output[state->active_output];
242242

243-
/* No switching allowed if explicitly disabled or in gaming mode */
244-
if (state->switch_lock || state->gaming_mode)
243+
/* No switching allowed if explicitly disabled */
244+
if (state->switch_lock)
245+
return;
246+
247+
/* In gaming mode, only allow switching if edge detection enabled and triggered */
248+
if (state->gaming_mode && !state->config.gaming_edge_enabled)
245249
return;
246250

247251
/* We want to jump in the direction of the other computer */
@@ -317,6 +321,47 @@ mouse_report_t create_mouse_report(device_t *state, mouse_values_t *values) {
317321
return mouse_report;
318322
}
319323

324+
enum screen_pos_e check_gaming_edge_switch(device_t *state, int offset_x) {
325+
// Feature disabled - return early
326+
if (!state->config.gaming_edge_enabled || !state->gaming_mode)
327+
return NONE;
328+
329+
output_t *output = &state->config.output[state->active_output];
330+
uint64_t now = time_us_64();
331+
uint64_t window_us = state->config.gaming_edge_window_ms * 1000;
332+
333+
// Determine which direction would switch screens
334+
enum screen_pos_e switch_direction = (output->pos == LEFT) ? RIGHT : LEFT;
335+
336+
// Check if movement is toward the other PC
337+
bool moving_toward_switch = (switch_direction == LEFT && offset_x < 0) ||
338+
(switch_direction == RIGHT && offset_x > 0);
339+
340+
// Reset accumulator if:
341+
// - Time window expired
342+
// - Moving in opposite direction
343+
if ((now - state->gaming_edge_last_reset) > window_us || !moving_toward_switch) {
344+
state->gaming_edge_accum = 0;
345+
state->gaming_edge_last_reset = now;
346+
347+
// If not moving toward switch, return early
348+
if (!moving_toward_switch)
349+
return NONE;
350+
}
351+
352+
// Accumulate movement (use absolute value)
353+
state->gaming_edge_accum += abs(offset_x);
354+
355+
// Check if threshold exceeded
356+
if (state->gaming_edge_accum >= state->config.gaming_edge_threshold) {
357+
state->gaming_edge_accum = 0;
358+
state->gaming_edge_last_reset = now;
359+
return switch_direction;
360+
}
361+
362+
return NONE;
363+
}
364+
320365
void process_mouse_report(uint8_t *raw_report, int len, uint8_t itf, hid_interface_t *iface) {
321366
mouse_values_t values = {0};
322367
device_t *state = &global_state;
@@ -327,6 +372,17 @@ void process_mouse_report(uint8_t *raw_report, int len, uint8_t itf, hid_interfa
327372
/* Calculate and update mouse pointer movement. */
328373
enum screen_pos_e switch_direction = update_mouse_position(state, &values);
329374

375+
/* Check for gaming mode edge switching */
376+
if (state->gaming_mode && state->config.gaming_edge_enabled) {
377+
// Use the acceleration-adjusted offset from update_mouse_position
378+
output_t *current = &state->config.output[state->active_output];
379+
uint8_t reduce_speed = state->mouse_zoom ? MOUSE_ZOOM_SCALING_FACTOR : 0;
380+
float acceleration_factor = calculate_mouse_acceleration_factor(values.move_x, values.move_y);
381+
int offset_x = round(values.move_x * acceleration_factor * (current->speed_x >> reduce_speed));
382+
383+
switch_direction = check_gaming_edge_switch(state, offset_x);
384+
}
385+
330386
/* Create the report for the output PC based on the updated values */
331387
mouse_report_t report = create_mouse_report(state, &values);
332388

@@ -376,4 +432,4 @@ void queue_mouse_report(mouse_report_t *report, device_t *state) {
376432
return;
377433

378434
queue_try_add(&state->mouse_queue, report);
379-
}
435+
}

src/protocol.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ const field_map_t api_field_map[] = {
5858
{ 75, false, UINT8, 1, offsetof(device_t, config.enable_acceleration) },
5959
{ 76, false, UINT8, 1, offsetof(device_t, config.enforce_ports) },
6060
{ 77, false, UINT16, 2, offsetof(device_t, config.jump_threshold) },
61+
{ 83, false, UINT8, 1, offsetof(device_t, config.gaming_mode_on_boot) },
62+
{ 84, false, UINT8, 1, offsetof(device_t, config.gaming_edge_enabled) },
63+
{ 85, false, UINT16, 2, offsetof(device_t, config.gaming_edge_threshold) },
64+
{ 86, false, UINT16, 2, offsetof(device_t, config.gaming_edge_window_ms) },
6165

6266
/* Firmware */
6367
{ 78, true, UINT16, 2, offsetof(device_t, _running_fw.version) },

0 commit comments

Comments
 (0)