diff --git a/.github/workflows/windows-cross-build-run.yml b/.github/workflows/windows-cross-build-run.yml index ca002958f13..e4da122c1da 100644 --- a/.github/workflows/windows-cross-build-run.yml +++ b/.github/workflows/windows-cross-build-run.yml @@ -171,6 +171,10 @@ jobs: # HTTP/1.1, which avoids the framing errors) and let cargo retry. CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" + # --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest + # lists Win11SDK_10.0.26100 without the per-arch header packages xwin + # 0.9.0 looks for, so the default splat fails with "unable to find + # x86_64 headers". Every target here builds against 22621. run: | set -e # Retry the whole install/splat a few times for non-cargo download @@ -178,7 +182,7 @@ jobs: n=0 until [ "$n" -ge 3 ]; do cargo install xwin --version 0.9.0 --locked \ - && xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ + && xwin --accept-license --manifest-version 16 --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ && break n=$((n+1)) echo "xwin install/splat attempt $n failed; retrying after backoff..." diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index 791fe526de3..15c9aaf7c88 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -17,6 +17,7 @@ on: - 'maven/windows/**' - 'scripts/gen-windows-uwp-stubs.py' - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java' - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' - '!vm/**/README.md' @@ -31,6 +32,7 @@ on: - 'maven/windows/**' - 'scripts/gen-windows-uwp-stubs.py' - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java' - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' @@ -94,19 +96,23 @@ jobs: uses: actions/setup-java@v5 with: { distribution: 'temurin', java-version: '8' } - - name: Install xwin + lay out the Windows SDK (x64) + - name: Install xwin + lay out the Windows SDK (x64 + arm64) env: # Harden against transient crates.io / download flakes ("curl [16] # HTTP2 framing layer", "[55] OpenSSL SSL_read unexpected eof"): # force HTTP/1.1 and let cargo retry. CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" + # --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest + # lists Win11SDK_10.0.26100 without the per-arch header packages xwin + # 0.9.0 looks for, so the default splat fails with "unable to find + # x86_64 headers". Every target here builds against 22621. run: | set -e n=0 until [ "$n" -ge 3 ]; do cargo install xwin --version 0.9.0 --locked \ - && xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ + && xwin --accept-license --manifest-version 16 --arch x86_64,aarch64 splat --output "$RUNNER_TEMP/xwin-sdk" \ && break n=$((n+1)) echo "xwin install/splat attempt $n failed; retrying after backoff..." @@ -178,12 +184,14 @@ jobs: CN1_CLANG_CL: clang-cl CN1_LLVM_RC: llvm-rc CN1_UWP_EXE_OUT: ${{ github.workspace }}/uwp-smoke/app.exe + CN1_KIOSK_EXE_OUT: ${{ github.workspace }}/kiosk-smoke/app.exe run: | cd vm JAVA_HOME="$JDK_8_HOME" mvn -B -pl tests -am test \ '-Dtest=CleanTargetIntegrationTest#crossCompilesWindowsExeWithXwin' \ '-Dsurefire.failIfNoSpecifiedTests=false' test -f "$CN1_UWP_EXE_OUT" + test -f "$CN1_KIOSK_EXE_OUT" # The development certificate windows.xbox builds create, made by the # builder's own openssl commands; the Windows job signs with it, so a @@ -209,6 +217,14 @@ jobs: # The manifest is pinned to WindowsNativeBuilder.buildXboxAppxManifest by # WindowsNativeBuilderArchTest, so the run below installs what windows.xbox # builds ship. + - name: Upload the kiosk smoke app + uses: actions/upload-artifact@v4 + with: + name: kiosk-smoke + path: kiosk-smoke/app.exe + if-no-files-found: error + retention-days: 3 + - name: Upload the UWP smoke app uses: actions/upload-artifact@v4 with: @@ -316,3 +332,97 @@ jobs: path: uwp-run/ if-no-files-found: ignore retention-days: 7 + + # The windows.embedded=true build on a real desktop: the window must cover the + # primary monitor with no frame, stay topmost, and survive the shortcuts the + # kiosk hook blocks. Shell Launcher itself needs IoT Enterprise, which no + # runner has; the deploy script is unit-tested in build-engine instead. + kiosk-run: + name: launch the kiosk build (Windows x64) + needs: cross-compile + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Download the kiosk smoke app + uses: actions/download-artifact@v4 + with: + name: kiosk-smoke + path: kiosk-smoke + + - name: Launch, check the window, send the blocked shortcuts + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Add-Type -AssemblyName System.Drawing, System.Windows.Forms + Add-Type @' + using System; + using System.Runtime.InteropServices; + public static class K { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } + [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra); + public static void Chord(byte mod, byte key) { + keybd_event(mod, 0, 0, UIntPtr.Zero); keybd_event(key, 0, 0, UIntPtr.Zero); + keybd_event(key, 0, 2, UIntPtr.Zero); keybd_event(mod, 0, 2, UIntPtr.Zero); + } + } + '@ + New-Item -ItemType Directory -Force kiosk-run | Out-Null + $log = Join-Path (Resolve-Path kiosk-run) 'app.log' + $env:CN1_LOG_FILE = $log + $p = Start-Process -FilePath (Resolve-Path kiosk-smoke/app.exe) -PassThru + $h = [IntPtr]::Zero + for ($i = 0; $i -lt 60 -and $h -eq [IntPtr]::Zero; $i++) { + Start-Sleep -Seconds 1 + if ($p.HasExited) { throw "the kiosk app exited with $($p.ExitCode) before showing a window" } + $p.Refresh(); $h = $p.MainWindowHandle + } + if ($h -eq [IntPtr]::Zero) { throw "the kiosk app showed no window in 60s" } + Start-Sleep -Seconds 3 + $style = [K]::GetWindowLong($h, -16); $ex = [K]::GetWindowLong($h, -20) + $r = New-Object K+RECT; [void][K]::GetWindowRect($h, [ref]$r) + $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + Write-Host ("style=0x{0:X8} ex=0x{1:X8} rect={2},{3},{4},{5} screen={6}" -f $style, $ex, $r.L, $r.T, $r.R, $r.B, $b) + $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height + $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); $g.Dispose() + $bmp.Save('kiosk-run/screen.png', [System.Drawing.Imaging.ImageFormat]::Png) + if (($style -band 0x80000000) -eq 0) { throw "the kiosk window is not WS_POPUP" } + if (($style -band 0x00C00000) -ne 0) { throw "the kiosk window has a caption" } + if (($ex -band 0x8) -eq 0) { throw "the kiosk window is not topmost" } + if ($r.L -ne $b.Left -or $r.T -ne $b.Top -or $r.R -ne $b.Right -or $r.B -ne $b.Bottom) { + throw "the kiosk window does not cover the primary monitor" + } + # Alt+F4 would close an ordinary window; the kiosk hook swallows it. + # The hook filters only while the kiosk window is in front, so the + # check means nothing unless it is: a refused SetForegroundWindow + # would send the chord to another window and pass vacuously. A lone + # Alt tap releases the foreground lock for this process. + for ($i = 0; $i -lt 10 -and [K]::GetForegroundWindow() -ne $h; $i++) { + [K]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero); [K]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero) + [void][K]::SetForegroundWindow($h) + Start-Sleep -Milliseconds 300 + } + if ([K]::GetForegroundWindow() -ne $h) { throw "the kiosk window never became the foreground window" } + [K]::Chord(0x12, 0x73) + [K]::Chord(0x12, 0x09) + Start-Sleep -Seconds 3 + if ($p.HasExited) { throw "Alt+F4 reached the kiosk window and closed it" } + if (-not (Test-Path $log)) { throw "CN1_LOG_FILE was not created" } + if ([K]::GetForegroundWindow() -ne $h) { throw "Alt+Tab moved the foreground away from the kiosk window" } + # Control: the same window does close when asked directly, so its + # surviving Alt+F4 above was the hook, not an app that ignores closing. + [void][K]::PostMessage($h, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero) + if (-not $p.WaitForExit(15000)) { Stop-Process -Id $p.Id -Force; throw "WM_CLOSE did not close the kiosk app" } + + - name: Upload the kiosk capture + if: always() + uses: actions/upload-artifact@v4 + with: + name: kiosk-run-capture + path: kiosk-run/ + if-no-files-found: ignore + retention-days: 7 diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java similarity index 90% rename from Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java rename to CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java index 1bdf0073f68..d5b0b5517e7 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java +++ b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.impl.linux; +package com.codename1.impl; import com.codename1.ui.Button; import com.codename1.ui.Component; @@ -35,13 +35,14 @@ import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.layouts.GridLayout; -/// Text editing on the embedded Linux target, where there is no native text -/// widget and usually no physical keyboard. A field being edited opens a +/// Text editing on the embedded targets (Linux on DRM/fbdev, Windows in kiosk +/// mode), where there is no native text widget or no system touch keyboard to +/// rely on, and usually no physical keyboard. A field being edited opens a /// dialog holding a lightweight copy of it and an on-screen keyboard; a /// physical keyboard types into the same field, so a panel with one works the /// same way. Done delivers the text through `Display.onEditingComplete`, /// Cancel restores the original. -final class LinuxEmbeddedEditor { +public final class EmbeddedTextEditor { private static final String[][] LETTERS = { {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, {"a", "s", "d", "f", "g", "h", "j", "k", "l", "'"}, @@ -53,10 +54,12 @@ final class LinuxEmbeddedEditor { {"!", "?", ";", "\"", "+", "=", "_", "<", ">", "~"} }; - private LinuxEmbeddedEditor() { + private EmbeddedTextEditor() { } - static void edit(final Component cmp, int maxSize, int constraint, final String text) { + /// Opens the editor for `cmp`; the arguments are those of + /// `CodenameOneImplementation.editString`. + public static void edit(final Component cmp, int maxSize, int constraint, final String text) { final boolean multiLine = cmp instanceof TextArea && !((TextArea) cmp).isSingleLineTextArea(); final TextField field = new TextField(text == null ? "" : text); field.setConstraint(constraint); @@ -78,10 +81,10 @@ static void edit(final Component cmp, int maxSize, int constraint, final String public void run() { keys.removeAll(); String[][] rows = symbols[0] ? SYMBOLS : LETTERS; - for (int r = 0; r < rows.length; r++) { - Container row = new Container(new GridLayout(1, rows[r].length)); - for (int k = 0; k < rows[r].length; k++) { - String label = rows[r][k]; + for (String[] keyRow : rows) { + Container row = new Container(new GridLayout(1, keyRow.length)); + for (String key : keyRow) { + String label = key; if (shift[0] && !symbols[0]) { label = upper(label); } @@ -150,6 +153,9 @@ public void actionPerformed(ActionEvent evt) { } } }); + // Docked to the bottom edge by showPacked below: framework chrome, never an + // operating system window of its own, whatever the theme defaults to. + dlg.setNativeWindowMode(false); dlg.setDisposeWhenPointerOutOfBounds(false); dlg.setScrollable(false); dlg.getContentPane().setPreferredH(Display.getInstance().getDisplayHeight() / 2); diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 5cd5d853eaa..904c17298d4 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -2587,7 +2587,7 @@ public boolean isAsyncEditMode() { @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { if (Embedded.VALUE) { - LinuxEmbeddedEditor.edit(cmp, maxSize, constraint, text); + com.codename1.impl.EmbeddedTextEditor.edit(cmp, maxSize, constraint, text); return; } lowLevelTextInput.stopCurrent(); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h index 828c9d1c815..55e34feb39a 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows.h +++ b/Ports/WindowsPort/nativeSources/cn1_windows.h @@ -355,6 +355,20 @@ extern CN1WindowsContext cn1Win; void cn1WindowsLog(const char* message); void cn1WinApplyPendingResize(void); int cn1WinCreateWindow(const char* utf8Title, int width, int height); + +/* The embedded (kiosk) target, cn1_windows_embedded.cpp. Each is a no-op in a + * build without CN1_WINDOWS_EMBEDDED. */ +int cn1WinEmbeddedActive(void); +/* Rotates the target monitor if asked, then answers the window style and the + * screen rectangle a kiosk window covers. */ +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc); +/* After the window is shown: topmost, the cursor policy, the keyboard filter. */ +void cn1WinEmbeddedWindowShown(HWND hwnd); +/* A forced density in DPI, or 0 for the monitor's own. */ +int cn1WinEmbeddedDpi(void); +/* Points stdout and stderr at CN1_LOG_FILE, which cn1:deploy-device sets: a + * shell has no console, and the device's log is what cn1:device-logs reads. */ +void cn1WinEmbeddedRedirectLog(void); void cn1WinPushEvent(CN1EventType type, int x, int y, int keyCode); /* Same, but tagged with the desktop window the event came from. */ void cn1WinPushWindowEvent(int windowId, CN1EventType type, int x, int y, int keyCode); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp new file mode 100644 index 00000000000..0337611d58e --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The embedded (kiosk) target: `windows.embedded=true`, compiled with + * CN1_WINDOWS_EMBEDDED. The application is the only thing on the screen of a + * Windows IoT Enterprise device -- usually as its shell, through Shell + * Launcher, which `cn1:deploy-device` configures: + * + * - one borderless, topmost window covering a whole monitor, no title bar; + * - the monitor rotated when the panel is mounted that way; + * - the Windows key, Alt+Tab, Alt+Esc, Ctrl+Esc and Alt+F4 filtered while the + * application has the foreground (Ctrl+Alt+Del is the Secure Attention + * Sequence and only Keyboard Filter can suppress it; deploy-device turns that + * on); + * - the cursor hidden on a touch panel; + * - text edited through the shared on-screen keyboard (EmbeddedTextEditor). + * + * The board profile's defaults arrive as CN1_DEFAULT_* defines, and each can be + * overridden on the device by the CN1_* environment variable of the same name, + * the convention of the embedded Linux target (cn1_linux_embedded.c). + */ +#include "cn1_windows.h" + +#ifdef _WIN32 + +#include +#include + +#ifndef CN1_DEFAULT_ROTATION +#define CN1_DEFAULT_ROTATION 0 +#endif +#ifndef CN1_DEFAULT_DPI +#define CN1_DEFAULT_DPI 0 +#endif +#ifndef CN1_DEFAULT_MONITOR +#define CN1_DEFAULT_MONITOR 0 +#endif +/* 1 shows the cursor (a panel with a mouse), 0 hides it (touch only). */ +#ifndef CN1_DEFAULT_CURSOR +#define CN1_DEFAULT_CURSOR 0 +#endif + +extern "C" { + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isEmbeddedDisplay___R_boolean(CODENAME_ONE_THREAD_STATE) { +#if defined(CN1_WINDOWS_EMBEDDED) && !defined(CN1_WINDOWS_UWP) + return JAVA_TRUE; +#else + return JAVA_FALSE; +#endif +} + +#if defined(CN1_WINDOWS_EMBEDDED) && !defined(CN1_WINDOWS_UWP) + +static int cn1EmbEnvInt(const char* name, int def) { + char buf[32]; + DWORD n = GetEnvironmentVariableA(name, buf, sizeof(buf)); + if (n == 0 || n >= sizeof(buf)) { + return def; + } + /* The hints are booleans for the cursor; accept the words as well as 1/0. */ + if (lstrcmpiA(buf, "true") == 0) { + return 1; + } + if (lstrcmpiA(buf, "false") == 0) { + return 0; + } + return atoi(buf); +} + +typedef struct { + int wanted; + int seen; + HMONITOR found; +} CN1EmbMonitorSearch; + +static BOOL CALLBACK cn1EmbMonitorProc(HMONITOR mon, HDC, LPRECT, LPARAM data) { + CN1EmbMonitorSearch* search = (CN1EmbMonitorSearch*) data; + if (search->seen == search->wanted) { + search->found = mon; + return FALSE; + } + search->seen++; + return TRUE; +} + +/* Monitor N in enumeration order; the primary when there is no such monitor. */ +static HMONITOR cn1EmbMonitor() { + CN1EmbMonitorSearch search; + search.wanted = cn1EmbEnvInt("CN1_MONITOR", CN1_DEFAULT_MONITOR); + search.seen = 0; + search.found = NULL; + if (search.wanted > 0) { + EnumDisplayMonitors(NULL, NULL, cn1EmbMonitorProc, (LPARAM) &search); + } + if (search.found == NULL) { + POINT origin = {0, 0}; + search.found = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY); + } + return search.found; +} + +/* Rotates the monitor for this session. CDS_FULLSCREEN makes the change + * temporary: Windows restores the panel's own orientation when the process + * exits, so a crash never leaves the device sideways for whatever runs next. */ +static void cn1EmbRotate(HMONITOR mon, int degrees) { + DWORD orientation; + switch (degrees) { + case 90: orientation = DMDO_90; break; + case 180: orientation = DMDO_180; break; + case 270: orientation = DMDO_270; break; + default: return; + } + MONITORINFOEXW info; + ZeroMemory(&info, sizeof(info)); + info.cbSize = sizeof(info); + if (!GetMonitorInfoW(mon, (MONITORINFO*) &info)) { + return; + } + DEVMODEW dm; + ZeroMemory(&dm, sizeof(dm)); + dm.dmSize = sizeof(dm); + if (!EnumDisplaySettingsW(info.szDevice, ENUM_CURRENT_SETTINGS, &dm)) { + return; + } + if (dm.dmDisplayOrientation == orientation) { + return; + } + /* A quarter turn from where the panel is swaps its width and height. */ + if (((dm.dmDisplayOrientation + orientation) & 1) != 0) { + DWORD w = dm.dmPelsWidth; + dm.dmPelsWidth = dm.dmPelsHeight; + dm.dmPelsHeight = w; + } + dm.dmDisplayOrientation = orientation; + dm.dmFields = DM_DISPLAYORIENTATION | DM_PELSWIDTH | DM_PELSHEIGHT; + LONG r = ChangeDisplaySettingsExW(info.szDevice, &dm, NULL, CDS_FULLSCREEN, NULL); + if (r != DISP_CHANGE_SUCCESSFUL) { + cn1WindowsLog("embedded: the display driver refused the rotation"); + } +} + +static HWND cn1EmbWindow; +static HHOOK cn1EmbKeyboardHook; + +/* Swallows the shell's own shortcuts while the kiosk window has the + * foreground. A low-level hook sees them before the shell does, which is the + * only place the Windows key can be stopped from user mode. */ +static LRESULT CALLBACK cn1EmbKeyboardProc(int code, WPARAM wParam, LPARAM lParam) { + if (code == HC_ACTION && GetForegroundWindow() == cn1EmbWindow) { + const KBDLLHOOKSTRUCT* k = (const KBDLLHOOKSTRUCT*) lParam; + BOOL alt = (k->flags & LLKHF_ALTDOWN) != 0; + BOOL ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + if (k->vkCode == VK_LWIN || k->vkCode == VK_RWIN + || (alt && (k->vkCode == VK_TAB || k->vkCode == VK_ESCAPE || k->vkCode == VK_F4)) + || (ctrl && k->vkCode == VK_ESCAPE)) { + return 1; + } + } + return CallNextHookEx(cn1EmbKeyboardHook, code, wParam, lParam); +} + +int cn1WinEmbeddedActive(void) { + return 1; +} + +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc) { + HMONITOR mon = cn1EmbMonitor(); + cn1EmbRotate(mon, cn1EmbEnvInt("CN1_ROTATION", CN1_DEFAULT_ROTATION)); + MONITORINFO info; + ZeroMemory(&info, sizeof(info)); + info.cbSize = sizeof(info); + GetMonitorInfoW(mon, &info); + /* The whole monitor, not its work area: under Shell Launcher there is no + * taskbar, and under any other shell the window covers it. */ + *rc = info.rcMonitor; + *style = WS_POPUP | WS_CLIPCHILDREN; + *exStyle = WS_EX_TOPMOST; +} + +void cn1WinEmbeddedWindowShown(HWND hwnd) { + cn1EmbWindow = hwnd; + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetForegroundWindow(hwnd); + if (cn1EmbEnvInt("CN1_CURSOR", CN1_DEFAULT_CURSOR) == 0) { + /* ShowCursor is a per-thread display counter; one decrement on the + * window's own thread hides it over the window. */ + ShowCursor(FALSE); + } + if (cn1EmbKeyboardHook == NULL) { + cn1EmbKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, cn1EmbKeyboardProc, GetModuleHandleW(NULL), 0); + if (cn1EmbKeyboardHook == NULL) { + cn1WindowsLog("embedded: keyboard filter not installed"); + } + } +} + +int cn1WinEmbeddedDpi(void) { + int dpi = cn1EmbEnvInt("CN1_DPI", CN1_DEFAULT_DPI); + return dpi > 0 ? dpi : 0; +} + +void cn1WinEmbeddedRedirectLog(void) { + char path[MAX_PATH]; + DWORD n = GetEnvironmentVariableA("CN1_LOG_FILE", path, sizeof(path)); + if (n == 0 || n >= sizeof(path)) { + return; + } + /* Plain freopen, not freopen_s: the _s variant opens the file without write + * sharing, so the second reopen of the same path failed, left stderr closed, + * and the setvbuf below then took the CRT's invalid-parameter fail-fast + * (0xC0000409) before the window existed. setvbuf only touches a stream + * that reopened. Unbuffered: the Microsoft CRT treats + * _IOLBF as full buffering, so a line buffer would reach cn1:device-logs a + * kilobyte at a time and lose the last output before a crash. */ +#pragma warning(suppress: 4996) + if (freopen(path, "a", stdout) != NULL) { + setvbuf(stdout, NULL, _IONBF, 0); + } else { + /* freopen closed the stream before failing; a later write to a closed + * stream is the same fail-fast, so park it on NUL instead. */ +#pragma warning(suppress: 4996) + freopen("NUL", "w", stdout); + } +#pragma warning(suppress: 4996) + if (freopen(path, "a", stderr) != NULL) { + setvbuf(stderr, NULL, _IONBF, 0); + } else { + /* freopen closed the stream before failing; a later write to a closed + * stream is the same fail-fast, so park it on NUL instead. */ +#pragma warning(suppress: 4996) + freopen("NUL", "w", stderr); + } +} + +#else /* a desktop or UWP build */ + +int cn1WinEmbeddedActive(void) { + return 0; +} + +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc) { +} + +void cn1WinEmbeddedWindowShown(HWND hwnd) { +} + +int cn1WinEmbeddedDpi(void) { + return 0; +} + +void cn1WinEmbeddedRedirectLog(void) { +} + +#endif + +} /* extern "C" */ + +#endif /* _WIN32 */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp new file mode 100644 index 00000000000..2367608734f --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp @@ -0,0 +1,1298 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * com.codename1.hardware on Windows (WindowsHardware.java). + * + * The Java side is the Linux one (KernelHardware): every primitive answers a + * handle or a result >= 0, or a negative *Linux* errno, so the error mapping + * and the edge/timeout protocol are shared rather than reinvented here. + * + * - Serial: Win32 COM ports (CreateFileW + DCB), on every Windows. + * - GPIO, I2C, SPI, PWM: the Windows.Devices.* WinRT APIs. They answer on + * Windows IoT Enterprise boards whose BSP ships the GpioClx/SpbCx drivers + * (NXP i.MX 8M, Qualcomm IoT, and x64 boards with a vendor driver) and on + * nothing else; an ordinary PC reports no controller, which reaches Java as + * ENODEV. + * - Backlight: DDC/CI (dxva2) for an external monitor, else WMI's + * WmiMonitorBrightness for an integrated panel -- the usual panel PC case. + * - CAN and the watchdog have no Windows standard: both are vendor SDKs, so + * they answer ENOTSUP and WindowsHardware does not advertise them. + * + * A UWP build (CN1_WINDOWS_UWP) gets the ENOTSUP half only: the app container + * has neither the COM-port nor the WMI path, and an Xbox has no buses. + */ +#include "cn1_windows.h" + +#ifdef _WIN32 + +#include +#include + +/* Linux errno values: KernelHardware formats and compares these. */ +#define CN1HW_EIO 5 +#define CN1HW_EBADF 9 +#define CN1HW_EBUSY 16 +#define CN1HW_ENODEV 19 +#define CN1HW_EINVAL 22 +#define CN1HW_EMFILE 24 +#define CN1HW_ENOTSUP 95 +#define CN1HW_ETIMEDOUT 110 + +#if !defined(CN1_WINDOWS_UWP) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Devices::Gpio; +using namespace ABI::Windows::Devices::I2c; +using namespace ABI::Windows::Devices::Spi; +using namespace ABI::Windows::Devices::Pwm; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; + +/* ------------------------------------------------------------ handle table + * + * One slot per open device. Slots are claimed and released under one SRW lock; + * the device calls themselves run outside it, on a reference the caller holds + * (AddRef'd interface, or the duplicated serial HANDLE), so a close racing a + * transfer cannot free what the transfer is using. */ + +enum { HW_FREE = 0, HW_GPIO, HW_I2C, HW_SPI, HW_SERIAL, HW_PWM }; + +#define CN1HW_SLOTS 128 +#define CN1HW_EDGE_RING 32 + +typedef struct CN1WinHwSlot { + int kind; + unsigned gen; /* bumped on every open: a waiter's handle is stale once it moves */ + IUnknown* device; /* IGpioPin / II2cDevice / ISpiDevice / IPwmPin */ + IUnknown* owner; /* IPwmController a PWM pin needs kept alive */ + HANDLE serial; + EventRegistrationToken edgeToken; + HANDLE edgeEvent; /* auto-reset; signalled per queued edge */ + int edgeHead; + int edgeCount; + int edgeRising[CN1HW_EDGE_RING]; + LONGLONG edgeNanos[CN1HW_EDGE_RING]; +} CN1WinHwSlot; + +static CN1WinHwSlot cn1WinHwSlots[CN1HW_SLOTS]; +static SRWLOCK cn1WinHwLock = SRWLOCK_INIT; + +static int cn1WinHwAlloc(int kind) { + int h = -CN1HW_EMFILE; + AcquireSRWLockExclusive(&cn1WinHwLock); + for (int i = 0; i < CN1HW_SLOTS; i++) { + if (cn1WinHwSlots[i].kind == HW_FREE) { + unsigned gen = cn1WinHwSlots[i].gen + 1; + ZeroMemory(&cn1WinHwSlots[i], sizeof(CN1WinHwSlot)); + cn1WinHwSlots[i].gen = gen; + cn1WinHwSlots[i].kind = kind; + cn1WinHwSlots[i].serial = INVALID_HANDLE_VALUE; + h = i; + break; + } + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +static unsigned cn1WinHwSlotGen(int h) { + AcquireSRWLockShared(&cn1WinHwLock); + unsigned gen = cn1WinHwSlots[h].gen; + ReleaseSRWLockShared(&cn1WinHwLock); + return gen; +} + +/* A referenced copy of the slot's device interface, or NULL for a stale or + * wrong-kind handle. The caller releases it. */ +static IUnknown* cn1WinHwRef(int h, int kind) { + IUnknown* d = NULL; + if (h < 0 || h >= CN1HW_SLOTS) { + return NULL; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == kind && cn1WinHwSlots[h].device != NULL) { + d = cn1WinHwSlots[h].device; + d->AddRef(); + } + ReleaseSRWLockShared(&cn1WinHwLock); + return d; +} + +/* A duplicate of a serial slot's HANDLE (closed by the caller), so the port + * stays open for a read that is still running when another thread closes it. */ +static HANDLE cn1WinHwSerialRef(int h) { + HANDLE out = INVALID_HANDLE_VALUE; + if (h < 0 || h >= CN1HW_SLOTS) { + return out; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == HW_SERIAL) { + if (!DuplicateHandle(GetCurrentProcess(), cn1WinHwSlots[h].serial, GetCurrentProcess(), &out, + 0, FALSE, DUPLICATE_SAME_ACCESS)) { + out = INVALID_HANDLE_VALUE; + } + } + ReleaseSRWLockShared(&cn1WinHwLock); + return out; +} + +static void cn1WinHwInit() { + /* Refcounted by the OS; the worker threads re-enter it cheaply. The paired + * RoUninitialize is skipped for the reason cn1_windows_winrt.cpp gives. */ + RoInitialize(RO_INIT_MULTITHREADED); +} + +static int cn1WinHwFromHr(HRESULT hr) { + if (hr == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) || hr == E_ACCESSDENIED + || hr == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)) { + return -CN1HW_EBUSY; + } + if (hr == HRESULT_FROM_WIN32(ERROR_TIMEOUT)) { + return -CN1HW_ETIMEDOUT; + } + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) + || hr == REGDB_E_CLASSNOTREG) { + return -CN1HW_ENODEV; + } + if (hr == E_INVALIDARG || hr == E_BOUNDS) { + return -CN1HW_EINVAL; + } + return -CN1HW_EIO; +} + +static int cn1WinHwFromWin32(DWORD err) { + switch (err) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return -CN1HW_ENODEV; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + return -CN1HW_EBUSY; + case ERROR_INVALID_PARAMETER: + return -CN1HW_EINVAL; + default: + return -CN1HW_EIO; + } +} + +/* Blocks the calling worker thread until a WinRT operation completes. The same + * helper as cn1_windows_winrt.cpp's, kept file-local as that one is. */ +/* The completion event belongs to the handler object, which the operation + * holds a reference to: an operation that completes after this function gave up + * on it signals an event that still exists, never a closed (and possibly + * reused) handle. */ +template +class CN1WinHwCompleted : public RuntimeClass, IAsyncOperationCompletedHandler> { +public: + HANDLE done; + CN1WinHwCompleted() : done(CreateEventExW(nullptr, nullptr, 0, EVENT_ALL_ACCESS)) {} + ~CN1WinHwCompleted() { + if (done != NULL) { + CloseHandle(done); + } + } + HRESULT STDMETHODCALLTYPE Invoke(IAsyncOperation*, AsyncStatus) override { + SetEvent(done); + return S_OK; + } +}; + +template +static HRESULT cn1WinHwAwait(IAsyncOperation* op, TResultPtr result) { + ComPtr> handler = Make>(); + if (handler == nullptr || handler->done == NULL) { + return E_FAIL; + } + HRESULT hr = op->put_Completed(handler.Get()); + if (FAILED(hr)) { + return hr; + } + if (WaitForSingleObject(handler->done, 30000) != WAIT_OBJECT_0) { + ComPtr info; + if (SUCCEEDED(op->QueryInterface(IID_PPV_ARGS(&info)))) { + info->Cancel(); + } + return HRESULT_FROM_WIN32(ERROR_TIMEOUT); + } + return op->GetResults(result); +} + +/* Monotonic nanoseconds, the clock the Linux port's edge timestamps use. */ +static LONGLONG cn1WinHwNanos() { + static LARGE_INTEGER freq; + LARGE_INTEGER now; + if (freq.QuadPart == 0) { + QueryPerformanceFrequency(&freq); + } + QueryPerformanceCounter(&now); + return (LONGLONG) ((double) now.QuadPart * 1000000000.0 / (double) freq.QuadPart); +} + +/* The device ids an AQS selector matches, in the order Windows enumerates them: + * bus N in the Java API is the Nth. Answers the count and fills ids[] (each a + * new HSTRING the caller deletes) up to max. */ +static int cn1WinHwFindDevices(HSTRING selector, HSTRING* ids, int max) { + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Enumeration_DeviceInformation).Get(), + IID_PPV_ARGS(&statics)))) { + return 0; + } + ComPtr> op; + if (FAILED(statics->FindAllAsyncAqsFilter(selector, &op))) { + return 0; + } + ComPtr> list; + /* Enumeration can take seconds; no Java object is touched while parked. */ + CN1_YIELD_THREAD; + HRESULT hr = cn1WinHwAwait(op.Get(), list.GetAddressOf()); + CN1_RESUME_THREAD; + if (FAILED(hr) || list == nullptr) { + return 0; + } + unsigned size = 0; + list->get_Size(&size); + for (unsigned i = 0; i < size && (int) i < max; i++) { + ComPtr info; + ids[i] = NULL; + if (SUCCEEDED(list->GetAt(i, &info))) { + info->get_Id(&ids[i]); + } + } + return (int) size; +} + +#define CN1HW_MAX_BUSES 16 + +/* The device id of bus `index` for a selector, or NULL. */ +static HSTRING cn1WinHwBusId(HSTRING selector, int index) { + HSTRING ids[CN1HW_MAX_BUSES]; + int n = cn1WinHwFindDevices(selector, ids, CN1HW_MAX_BUSES); + HSTRING out = NULL; + for (int i = 0; i < n && i < CN1HW_MAX_BUSES; i++) { + if (i == index) { + out = ids[i]; + } else if (ids[i] != NULL) { + WindowsDeleteString(ids[i]); + } + } + return out; +} + +extern "C" { + +/* ------------------------------------------------------------------ GPIO */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, + JAVA_INT bias, JAVA_INT edges) { + /* Windows exposes one GPIO controller to applications (GpioController's + * default, which a BSP may back with several banks numbered contiguously), + * so the chip name is not a selector here: WindowsHardware lists it as + * "gpio0", and any name opens it. */ + (void) chip; + if (line < 0) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Gpio_GpioController).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + ComPtr controller; + if (FAILED(statics->GetDefault(&controller)) || controller == nullptr) { + return -CN1HW_ENODEV; + } + ComPtr pin; + GpioOpenStatus status = GpioOpenStatus_PinUnavailable; + boolean opened = false; + HRESULT hr = controller->TryOpenPin(line, GpioSharingMode_Exclusive, &pin, &status, &opened); + if (FAILED(hr) || !opened || pin == nullptr) { + return status == GpioOpenStatus_SharingViolation ? -CN1HW_EBUSY : -CN1HW_ENODEV; + } + GpioPinDriveMode mode = GpioPinDriveMode_Input; + if (output) { + mode = GpioPinDriveMode_Output; + /* Latch the level first so the pin never glitches the other way when + * it turns into an output. */ + pin->Write(high ? GpioPinValue_High : GpioPinValue_Low); + } else if (bias == 2) { + mode = GpioPinDriveMode_InputPullUp; + } else if (bias == 3) { + mode = GpioPinDriveMode_InputPullDown; + } + boolean supported = false; + if (FAILED(pin->IsDriveModeSupported(mode, &supported)) || !supported) { + return -CN1HW_ENOTSUP; + } + if (FAILED(pin->SetDriveMode(mode))) { + return -CN1HW_EIO; + } + int h = cn1WinHwAlloc(HW_GPIO); + if (h < 0) { + return h; + } + if (!output && edges != 0) { + HANDLE ev = CreateEventW(NULL, FALSE, FALSE, NULL); + int slot = h; + unsigned gen = cn1WinHwSlotGen(h); + EventRegistrationToken token; + hr = pin->add_ValueChanged(Callback>( + [slot, gen, edges](IGpioPin*, IGpioPinValueChangedEventArgs* args) -> HRESULT { + GpioPinEdge edge = GpioPinEdge_FallingEdge; + args->get_Edge(&edge); + int rising = edge == GpioPinEdge_RisingEdge ? 1 : 0; + if ((rising && (edges & 1) == 0) || (!rising && (edges & 2) == 0)) { + return S_OK; + } + LONGLONG ns = cn1WinHwNanos(); + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot* s = &cn1WinHwSlots[slot]; + /* remove_ValueChanged does not wait for a callback already + * running: a late edge of a closed pin must not land in the + * pin that reopened this slot. */ + if (s->kind == HW_GPIO && s->gen == gen && s->edgeEvent != NULL) { + /* A full ring drops the oldest edge, as the kernel's + * line-event buffer does. */ + int tail = (s->edgeHead + s->edgeCount) % CN1HW_EDGE_RING; + if (s->edgeCount == CN1HW_EDGE_RING) { + s->edgeHead = (s->edgeHead + 1) % CN1HW_EDGE_RING; + } else { + s->edgeCount++; + } + s->edgeRising[tail] = rising; + s->edgeNanos[tail] = ns; + SetEvent(s->edgeEvent); + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + return S_OK; + }).Get(), &token); + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].edgeEvent = ev; + cn1WinHwSlots[h].edgeToken = token; + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (FAILED(hr)) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].kind = HW_FREE; + cn1WinHwSlots[h].edgeEvent = NULL; + ReleaseSRWLockExclusive(&cn1WinHwLock); + CloseHandle(ev); + return -CN1HW_ENOTSUP; + } + } + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = pin.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + IUnknown* d = cn1WinHwRef(h, HW_GPIO); + if (d == NULL) { + return -CN1HW_EBADF; + } + GpioPinValue v = GpioPinValue_Low; + HRESULT hr = ((IGpioPin*) d)->Read(&v); + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : (v == GpioPinValue_High ? 1 : 0); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN high) { + IUnknown* d = cn1WinHwRef(h, HW_GPIO); + if (d == NULL) { + return -CN1HW_EBADF; + } + HRESULT hr = ((IGpioPin*) d)->Write(high ? GpioPinValue_High : GpioPinValue_Low); + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + HANDLE ev = NULL; + unsigned gen = 0; + if (h < 0 || h >= CN1HW_SLOTS) { + return -CN1HW_EBADF; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == HW_GPIO && cn1WinHwSlots[h].edgeEvent != NULL) { + gen = cn1WinHwSlots[h].gen; + DuplicateHandle(GetCurrentProcess(), cn1WinHwSlots[h].edgeEvent, GetCurrentProcess(), &ev, 0, FALSE, + DUPLICATE_SAME_ACCESS); + } + ReleaseSRWLockShared(&cn1WinHwLock); + if (ev == NULL) { + return -CN1HW_EBADF; + } + int r = -CN1HW_ETIMEDOUT; + LONGLONG ns = 0; + DWORD deadline = GetTickCount() + (DWORD) (timeoutMs < 0 ? 0 : timeoutMs); + for (;;) { + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot* s = &cn1WinHwSlots[h]; + if (s->kind != HW_GPIO || s->gen != gen) { + /* Closed, or closed and reopened as another pin while we waited. */ + r = -CN1HW_EBADF; + } else if (s->edgeCount > 0) { + r = s->edgeRising[s->edgeHead]; + ns = s->edgeNanos[s->edgeHead]; + s->edgeHead = (s->edgeHead + 1) % CN1HW_EDGE_RING; + s->edgeCount--; + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (r != -CN1HW_ETIMEDOUT) { + break; + } + DWORD wait = INFINITE; + if (timeoutMs >= 0) { + DWORD now = GetTickCount(); + if ((LONG) (deadline - now) <= 0) { + break; + } + wait = deadline - now; + } + CN1_YIELD_THREAD; + DWORD w = WaitForSingleObject(ev, wait); + CN1_RESUME_THREAD; + if (w == WAIT_FAILED) { + r = -CN1HW_EIO; + break; + } + if (w != WAIT_OBJECT_0 && timeoutMs >= 0) { + break; + } + } + CloseHandle(ev); + if (r >= 0 && tsOut != JAVA_NULL && CN1_ARRAY_LENGTH(tsOut) > 0) { + ((JAVA_LONG*) CN1_ARRAY_DATA(tsOut))[0] = (JAVA_LONG) ns; + } + return r; +} + +/* ------------------------------------------------------------------- I2C */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, + JAVA_INT address) { + if (bus < 0 || address < 0 || address > 0x3ff) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cDevice).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), bus); + if (id == NULL) { + return -CN1HW_ENODEV; + } + // Windows.Devices.I2c addresses 7-bit devices only; there is no 10-bit mode. + if (address > 0x7f) { + WindowsDeleteString(id); + return -CN1HW_ENOTSUP; + } + ComPtr settingsFactory; + ComPtr settings; + HRESULT hr = RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cConnectionSettings).Get(), + IID_PPV_ARGS(&settingsFactory)); + if (SUCCEEDED(hr)) { + hr = settingsFactory->Create(address, &settings); + } + ComPtr> op; + if (SUCCEEDED(hr)) { + hr = statics->FromIdAsync(id, settings.Get(), &op); + } + WindowsDeleteString(id); + ComPtr device; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), device.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr)) { + return cn1WinHwFromHr(hr); + } + if (device == nullptr) { + /* FromIdAsync answers null for a bus another process holds. */ + return -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_I2C); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = device.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + if (wl < 0 || rl < 0 || (wl > 0 && (w == JAVA_NULL || wl > CN1_ARRAY_LENGTH(w))) + || (rl > 0 && (rd == JAVA_NULL || rl > CN1_ARRAY_LENGTH(rd))) || (wl == 0 && rl == 0)) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_I2C); + if (d == NULL) { + return -CN1HW_EBADF; + } + II2cDevice* dev = (II2cDevice*) d; + BYTE* wp = wl > 0 ? (BYTE*) CN1_ARRAY_DATA(w) : NULL; + BYTE* rp = rl > 0 ? (BYTE*) CN1_ARRAY_DATA(rd) : NULL; + HRESULT hr; + CN1_YIELD_THREAD; + if (wl > 0 && rl > 0) { + hr = dev->WriteRead((UINT32) wl, wp, (UINT32) rl, rp); + } else if (wl > 0) { + hr = dev->Write((UINT32) wl, wp); + } else { + hr = dev->Read((UINT32) rl, rp); + } + CN1_RESUME_THREAD; + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* ------------------------------------------------------------------- SPI */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + if (bus < 0 || cs < 0 || mode < 0 || mode > 3 || speed <= 0 || bits < 1 || bits > 32) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiDevice).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), bus); + if (id == NULL) { + return -CN1HW_ENODEV; + } + ComPtr settingsFactory; + ComPtr settings; + HRESULT hr = RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiConnectionSettings).Get(), + IID_PPV_ARGS(&settingsFactory)); + if (SUCCEEDED(hr)) { + hr = settingsFactory->Create(cs, &settings); + } + if (SUCCEEDED(hr)) { + settings->put_Mode((SpiMode) mode); + settings->put_ClockFrequency(speed); + settings->put_DataBitLength(bits); + } + ComPtr> op; + if (SUCCEEDED(hr)) { + hr = statics->FromIdAsync(id, settings.Get(), &op); + } + WindowsDeleteString(id); + ComPtr device; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), device.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr)) { + return cn1WinHwFromHr(hr); + } + if (device == nullptr) { + return -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_SPI); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = device.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + if (len <= 0 || (tx != JAVA_NULL && len > CN1_ARRAY_LENGTH(tx)) || (rx != JAVA_NULL && len > CN1_ARRAY_LENGTH(rx)) + || (tx == JAVA_NULL && rx == JAVA_NULL)) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_SPI); + if (d == NULL) { + return -CN1HW_EBADF; + } + ISpiDevice* dev = (ISpiDevice*) d; + HRESULT hr; + CN1_YIELD_THREAD; + if (tx != JAVA_NULL && rx != JAVA_NULL) { + hr = dev->TransferFullDuplex((UINT32) len, (BYTE*) CN1_ARRAY_DATA(tx), (UINT32) len, (BYTE*) CN1_ARRAY_DATA(rx)); + } else if (tx != JAVA_NULL) { + hr = dev->Write((UINT32) len, (BYTE*) CN1_ARRAY_DATA(tx)); + } else { + hr = dev->Read((UINT32) len, (BYTE*) CN1_ARRAY_DATA(rx)); + } + CN1_RESUME_THREAD; + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* ---------------------------------------------------------------- serial */ + +/* "COM3" or "\\.\COM3", and nothing else: the port opens a COM device, never a + * file or another DOS device an application names. Answers the number. */ +static int cn1WinHwComNumber(const char* name) { + if (name == NULL) { + return -1; + } + if (strncmp(name, "\\\\.\\", 4) == 0) { + name += 4; + } + if ((name[0] != 'C' && name[0] != 'c') || (name[1] != 'O' && name[1] != 'o') || (name[2] != 'M' && name[2] != 'm')) { + return -1; + } + int n = 0; + const char* p = name + 3; + if (*p == 0) { + return -1; + } + for (; *p; p++) { + if (*p < '0' || *p > '9' || n > 1000) { + return -1; + } + n = n * 10 + (*p - '0'); + } + return n > 0 ? n : -1; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialOpen___java_lang_String_int_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + int com = cn1WinHwComNumber(dev == JAVA_NULL ? NULL : stringToUTF8(threadStateData, dev)); + if (com < 0 || baud <= 0 || dataBits < 5 || dataBits > 8 || stopBits < 1 || stopBits > 2 || parity < 0 || parity > 2) { + return -CN1HW_EINVAL; + } + WCHAR path[32]; + wsprintfW(path, L"\\\\.\\COM%d", com); + HANDLE port = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (port == INVALID_HANDLE_VALUE) { + return cn1WinHwFromWin32(GetLastError()); + } + DCB dcb; + ZeroMemory(&dcb, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(port, &dcb)) { + CloseHandle(port); + return -CN1HW_EIO; + } + /* Raw, like cfmakeraw on Linux: binary, no flow control, no character + * substitution. */ + dcb.BaudRate = (DWORD) baud; + dcb.ByteSize = (BYTE) dataBits; + dcb.Parity = parity == 1 ? ODDPARITY : parity == 2 ? EVENPARITY : NOPARITY; + dcb.StopBits = stopBits == 2 ? TWOSTOPBITS : ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fParity = parity != 0; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fDsrSensitivity = FALSE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fErrorChar = FALSE; + dcb.fNull = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + dcb.fAbortOnError = FALSE; + if (!SetCommState(port, &dcb)) { + CloseHandle(port); + return -CN1HW_EINVAL; + } + PurgeComm(port, PURGE_RXCLEAR | PURGE_TXCLEAR); + int h = cn1WinHwAlloc(HW_SERIAL); + if (h < 0) { + CloseHandle(port); + return h; + } + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].serial = port; + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialRead___int_byte_1ARRAY_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -CN1HW_EINVAL; + } + HANDLE port = cn1WinHwSerialRef(h); + if (port == INVALID_HANDLE_VALUE) { + return -CN1HW_EBADF; + } + if (len == 0) { + CloseHandle(port); + return 0; + } + /* A timeout with nothing read answers 0, not -ETIMEDOUT: the Linux port's + * hwSerialRead (cn1_hardware_impl.h) does the same, and KernelHardware reads + * 0 as "nothing yet" on both. + * Return as soon as any byte is there, or after timeoutMs with nothing: the + * documented MAXDWORD/MAXDWORD/constant combination. A negative timeout + * waits indefinitely, which the constant cannot say, so it is a long one. */ + COMMTIMEOUTS t; + ZeroMemory(&t, sizeof(t)); + t.ReadIntervalTimeout = MAXDWORD; + t.ReadTotalTimeoutMultiplier = MAXDWORD; + t.ReadTotalTimeoutConstant = timeoutMs < 0 ? (MAXDWORD - 1) : (timeoutMs == 0 ? 1 : (DWORD) timeoutMs); + t.WriteTotalTimeoutConstant = 0; + SetCommTimeouts(port, &t); + DWORD n = 0; + CN1_YIELD_THREAD; + BOOL ok = ReadFile(port, (char*) CN1_ARRAY_DATA(buf) + off, (DWORD) len, &n, NULL); + CN1_RESUME_THREAD; + DWORD err = ok ? 0 : GetLastError(); + CloseHandle(port); + return ok ? (JAVA_INT) n : cn1WinHwFromWin32(err); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialWrite___int_byte_1ARRAY_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -CN1HW_EINVAL; + } + HANDLE port = cn1WinHwSerialRef(h); + if (port == INVALID_HANDLE_VALUE) { + return -CN1HW_EBADF; + } + int written = 0; + int r = 0; + CN1_YIELD_THREAD; + while (written < len) { + DWORD n = 0; + if (!WriteFile(port, (char*) CN1_ARRAY_DATA(buf) + off + written, (DWORD) (len - written), &n, NULL)) { + r = cn1WinHwFromWin32(GetLastError()); + break; + } + written += (int) n; + } + CN1_RESUME_THREAD; + CloseHandle(port); + return r < 0 ? r : written; +} + +/* ------------------------------------------------------------------- PWM */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, + JAVA_INT channel) { + if (chip < 0 || channel < 0) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Pwm_PwmController).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), chip); + if (id == NULL) { + return -CN1HW_ENODEV; + } + ComPtr> op; + HRESULT hr = statics->FromIdAsync(id, &op); + WindowsDeleteString(id); + ComPtr controller; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), controller.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr) || controller == nullptr) { + return FAILED(hr) ? cn1WinHwFromHr(hr) : -CN1HW_EBUSY; + } + INT32 count = 0; + controller->get_PinCount(&count); + if (channel >= count) { + return -CN1HW_ENODEV; + } + ComPtr pin; + hr = controller->OpenPin(channel, &pin); + if (FAILED(hr) || pin == nullptr) { + return FAILED(hr) ? cn1WinHwFromHr(hr) : -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_PWM); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = pin.Detach(); + cn1WinHwSlots[h].owner = controller.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + if (period <= 0 || duty < 0 || duty > period) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_PWM); + if (d == NULL) { + return -CN1HW_EBADF; + } + IPwmPin* pin = (IPwmPin*) d; + ComPtr controller; + HRESULT hr = pin->get_Controller(&controller); + if (SUCCEEDED(hr)) { + /* Windows sets the frequency per controller, not per channel: two + * channels of one controller share it, which the documentation of + * the Windows target states. */ + double actual = 0; + hr = controller->SetDesiredFrequency(1000000000.0 / (double) period, &actual); + } + if (SUCCEEDED(hr)) { + hr = pin->SetActiveDutyCyclePercentage((double) duty / (double) period); + } + if (SUCCEEDED(hr)) { + boolean started = false; + pin->get_IsStarted(&started); + if (enabled && !started) { + hr = pin->Start(); + } else if (!enabled && started) { + hr = pin->Stop(); + } + } + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* -------------------------------------------------- CAN and the watchdog */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_OBJECT iface) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanSend___int_int_boolean_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT timeoutSeconds) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +/* ----------------------------------------------------------------- close */ + +JAVA_VOID com_codename1_impl_windows_WindowsNative_hwClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN disarm) { + (void) disarm; /* only a watchdog disarms, and there is none here */ + if (h < 0 || h >= CN1HW_SLOTS) { + return; + } + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot s = cn1WinHwSlots[h]; + cn1WinHwSlots[h].kind = HW_FREE; + cn1WinHwSlots[h].device = NULL; + cn1WinHwSlots[h].owner = NULL; + cn1WinHwSlots[h].serial = INVALID_HANDLE_VALUE; + cn1WinHwSlots[h].edgeEvent = NULL; + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (s.kind == HW_FREE) { + return; + } + if (s.kind == HW_GPIO && s.device != NULL && s.edgeEvent != NULL) { + ((IGpioPin*) s.device)->remove_ValueChanged(s.edgeToken); + } + if (s.edgeEvent != NULL) { + /* Wake a hwGpioWaitEdge parked on its duplicate of this event; it then + * sees the slot freed and answers EBADF instead of waiting forever. */ + SetEvent(s.edgeEvent); + } + if (s.kind == HW_PWM && s.device != NULL) { + ((IPwmPin*) s.device)->Stop(); + } + if (s.device != NULL) { + /* Close releases the pin or bus for other processes now, not when the + * last reference a transfer may still hold goes away. */ + ComPtr closable; + if (SUCCEEDED(s.device->QueryInterface(IID_PPV_ARGS(&closable)))) { + closable->Close(); + } + s.device->Release(); + } + if (s.owner != NULL) { + s.owner->Release(); + } + if (s.serial != INVALID_HANDLE_VALUE) { + CloseHandle(s.serial); + } + if (s.edgeEvent != NULL) { + /* Wakes a waiter, which then finds the slot gone and answers EBADF. */ + SetEvent(s.edgeEvent); + CloseHandle(s.edgeEvent); + } +} + +/* ----------------------------------------------------------------- lists */ + +static void cn1WinHwAppend(char* out, size_t cap, const char* line) { + size_t used = strlen(out); + size_t n = strlen(line); + if (used + n + 2 < cap) { + if (used > 0) { + out[used++] = '\n'; + } + memcpy(out + used, line, n + 1); + } +} + +static void cn1WinHwListBuses(HSTRING selector, const char* prefix, char* out, size_t cap) { + HSTRING ids[CN1HW_MAX_BUSES]; + int n = cn1WinHwFindDevices(selector, ids, CN1HW_MAX_BUSES); + for (int i = 0; i < n && i < CN1HW_MAX_BUSES; i++) { + char line[32]; + wsprintfA(line, "%s%d", prefix, i); + cn1WinHwAppend(out, cap, line); + if (ids[i] != NULL) { + WindowsDeleteString(ids[i]); + } + } +} + +/* kind: 0 GPIO controllers, 1 I2C buses, 2 SPI buses, 3 serial ports, 4 CAN. + * Newline separated, in the shape WindowsHardware parses: "gpio0", "i2c-N", + * "spi-N", "COMn". */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_hwList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, + JAVA_INT kind) { + char out[4096]; + out[0] = 0; + cn1WinHwInit(); + if (kind == 0) { + ComPtr statics; + ComPtr controller; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Gpio_GpioController).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDefault(&controller)) && controller != nullptr) { + cn1WinHwAppend(out, sizeof(out), "gpio0"); + } + } else if (kind == 1) { + ComPtr statics; + HString selector; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cDevice).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + cn1WinHwListBuses(selector.Get(), "i2c-", out, sizeof(out)); + } + } else if (kind == 2) { + ComPtr statics; + HString selector; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiDevice).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + cn1WinHwListBuses(selector.Get(), "spi-", out, sizeof(out)); + } + } else if (kind == 3) { + /* Every DOS device name, filtered to COMn: no SetupAPI needed. */ + DWORD cap = 65536; + WCHAR* names = (WCHAR*) malloc(cap * sizeof(WCHAR)); + if (names != NULL && QueryDosDeviceW(NULL, names, cap) != 0) { + for (WCHAR* p = names; *p; p += wcslen(p) + 1) { + char narrow[64]; + if (WideCharToMultiByte(CP_UTF8, 0, p, -1, narrow, sizeof(narrow), NULL, NULL) > 0 + && cn1WinHwComNumber(narrow) > 0) { + cn1WinHwAppend(out, sizeof(out), narrow); + } + } + } + free(names); + } + return newStringFromCString(threadStateData, out); +} + +/* ------------------------------------------------------------- backlight + * + * op: 0 current, 1 maximum, 2 set. DDC/CI first (an external monitor that + * answers it), then WMI's WmiMonitorBrightness, which is what an integrated + * panel -- a panel PC, a tablet -- reports through. WMI's scale is 0..100. */ + +static int cn1WinHwDdc(int op, int value) { + HMONITOR mon = MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY); + DWORD count = 0; + if (!GetNumberOfPhysicalMonitorsFromHMONITOR(mon, &count) || count == 0) { + return -CN1HW_ENODEV; + } + PHYSICAL_MONITOR* monitors = (PHYSICAL_MONITOR*) calloc(count, sizeof(PHYSICAL_MONITOR)); + if (monitors == NULL || !GetPhysicalMonitorsFromHMONITOR(mon, count, monitors)) { + free(monitors); + return -CN1HW_ENODEV; + } + DWORD lo = 0, cur = 0, hi = 0; + int r = -CN1HW_ENODEV; + if (GetMonitorBrightness(monitors[0].hPhysicalMonitor, &lo, &cur, &hi)) { + if (op == 0) { + r = (int) cur; + } else if (op == 1) { + r = (int) hi; + } else { + DWORD v = value < (int) lo ? lo : (value > (int) hi ? hi : (DWORD) value); + r = SetMonitorBrightness(monitors[0].hPhysicalMonitor, v) ? 0 : -CN1HW_EIO; + } + } + DestroyPhysicalMonitors(count, monitors); + free(monitors); + return r; +} + +static int cn1WinHwWmi(int op, int value) { + if (op == 1) { + return 100; + } + CoInitializeEx(NULL, COINIT_MULTITHREADED); + ComPtr locator; + if (FAILED(CoCreateInstance(__uuidof(WbemLocator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&locator)))) { + return -CN1HW_ENODEV; + } + ComPtr services; + BSTR ns = SysAllocString(L"ROOT\\WMI"); + HRESULT hr = locator->ConnectServer(ns, NULL, NULL, NULL, 0, NULL, NULL, &services); + SysFreeString(ns); + if (FAILED(hr)) { + return -CN1HW_ENODEV; + } + CoSetProxyBlanket(services.Get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, + RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); + BSTR lang = SysAllocString(L"WQL"); + BSTR query = SysAllocString(op == 0 ? L"SELECT CurrentBrightness FROM WmiMonitorBrightness" + : L"SELECT * FROM WmiMonitorBrightnessMethods"); + ComPtr results; + hr = services->ExecQuery(lang, query, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &results); + SysFreeString(lang); + SysFreeString(query); + if (FAILED(hr)) { + return -CN1HW_ENODEV; + } + ComPtr obj; + ULONG got = 0; + if (FAILED(results->Next(WBEM_INFINITE, 1, &obj, &got)) || got == 0) { + return -CN1HW_ENODEV; + } + VARIANT v; + VariantInit(&v); + if (op == 0) { + int r = -CN1HW_EIO; + if (SUCCEEDED(obj->Get(L"CurrentBrightness", 0, &v, NULL, NULL))) { + r = v.vt == VT_UI1 ? (int) v.bVal : (v.vt == VT_I4 ? (int) v.lVal : -CN1HW_EIO); + } + VariantClear(&v); + return r; + } + /* WmiSetBrightness(Timeout uint32, Brightness uint8) on the instance. */ + if (FAILED(obj->Get(L"__PATH", 0, &v, NULL, NULL)) || v.vt != VT_BSTR) { + VariantClear(&v); + return -CN1HW_EIO; + } + BSTR path = SysAllocString(v.bstrVal); + VariantClear(&v); + ComPtr cls; + ComPtr inDef; + ComPtr in; + BSTR clsName = SysAllocString(L"WmiMonitorBrightnessMethods"); + BSTR method = SysAllocString(L"WmiSetBrightness"); + hr = services->GetObject(clsName, 0, NULL, &cls, NULL); + if (SUCCEEDED(hr)) { + hr = cls->GetMethod(method, 0, &inDef, NULL); + } + if (SUCCEEDED(hr)) { + hr = inDef->SpawnInstance(0, &in); + } + if (SUCCEEDED(hr)) { + VARIANT a; + VariantInit(&a); + a.vt = VT_I4; + a.lVal = 0; + in->Put(L"Timeout", 0, &a, 0); + a.vt = VT_UI1; + a.bVal = (BYTE) (value < 0 ? 0 : (value > 100 ? 100 : value)); + in->Put(L"Brightness", 0, &a, 0); + hr = services->ExecMethod(path, method, 0, NULL, in.Get(), NULL, NULL); + } + SysFreeString(clsName); + SysFreeString(method); + SysFreeString(path); + return FAILED(hr) ? -CN1HW_EIO : 0; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, + JAVA_INT value) { + int r = cn1WinHwDdc(op, value); + if (r == -CN1HW_ENODEV) { + r = cn1WinHwWmi(op, value); + } + return r; +} + +} /* extern "C" */ + +#else /* CN1_WINDOWS_UWP: no COM ports, no WMI, no buses on an Xbox. */ + +extern "C" { + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, + JAVA_INT bias, JAVA_INT edges) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN high) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, + JAVA_INT address) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialOpen___java_lang_String_int_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialRead___int_byte_1ARRAY_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialWrite___int_byte_1ARRAY_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, + JAVA_INT channel) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_OBJECT iface) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanSend___int_int_boolean_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT timeoutSeconds) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +JAVA_VOID com_codename1_impl_windows_WindowsNative_hwClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN disarm) { +} + +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_hwList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, + JAVA_INT kind) { + return newStringFromCString(threadStateData, ""); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, + JAVA_INT value) { + return -CN1HW_ENODEV; +} + +} /* extern "C" */ + +#endif /* CN1_WINDOWS_UWP */ + +#endif /* _WIN32 */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp index 53cc731b0a5..0e5473843dc 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp @@ -972,8 +972,22 @@ int cn1WinCreateWindow(const char* utf8Title, int width, int height) { /* WS_CLIPCHILDREN so the Direct2D present does not paint over native child * controls overlaid on the form (the WebView2 browser peer and the EDIT * control used for native text editing). */ - cn1Win.hwnd = CreateWindowExW(0, L"CodenameOneWindow", wTitle, - WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, width, height, + DWORD style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + DWORD exStyle = 0; + int x = CW_USEDEFAULT; + int y = CW_USEDEFAULT; + /* The kiosk target covers a whole monitor with a borderless window + * (cn1_windows_embedded.cpp); the requested size is the desktop's. */ + if (cn1WinEmbeddedActive() && !cn1Win.offscreenCapture) { + RECT rc; + cn1WinEmbeddedPlaceWindow(&style, &exStyle, &rc); + x = rc.left; + y = rc.top; + width = rc.right - rc.left; + height = rc.bottom - rc.top; + } + cn1Win.hwnd = CreateWindowExW(exStyle, L"CodenameOneWindow", wTitle, + style, x, y, width, height, NULL, NULL, hInstance, NULL); free(wTitle); return cn1Win.hwnd != NULL ? 1 : 0; @@ -1120,6 +1134,10 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin } + if (cn1WinEmbeddedActive()) { + cn1WinEmbeddedRedirectLog(); + } + /* Headless screenshot mode: no window; render into an offscreen WIC bitmap * that headlessTick later encodes to PNG. The EDT paints through the same * getWindowGraphics + flushGraphics path as on-screen. */ @@ -1205,6 +1223,9 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin if (!cn1Win.offscreenCapture) { ShowWindow(cn1Win.hwnd, SW_SHOW); UpdateWindow(cn1Win.hwnd); + if (cn1WinEmbeddedActive()) { + cn1WinEmbeddedWindowShown(cn1Win.hwnd); + } cn1WindowsLog("initDisplay: window shown"); } else { cn1WindowsLog("initDisplay: offscreen capture -- window kept hidden"); @@ -1219,6 +1240,12 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_getDisplayWidth___R_int(CODENA * mm-based theme metrics correctly instead of using the mobile density * heuristic, which over-scales everything. */ JAVA_INT com_codename1_impl_windows_WindowsNative_screenDpi___R_int(CODENAME_ONE_THREAD_STATE) { + /* A kiosk panel that misreports its size gets its density from the board + * profile or CN1_DPI instead (cn1_windows_embedded.cpp). */ + int forced = cn1WinEmbeddedDpi(); + if (forced > 0) { + return (JAVA_INT) forced; + } // LOGPIXELSX reports system DPI even for a per-monitor-aware window. typedef UINT (WINAPI *GetDpiFn)(HWND); GetDpiFn getDpi = (GetDpiFn)GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java new file mode 100644 index 00000000000..f0f5466ef12 --- /dev/null +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.windows; + +import com.codename1.impl.KernelHardware; + +/// The Windows port's [com.codename1.hardware.Hardware]. It reuses +/// [KernelHardware]'s error and edge protocol -- the natives in +/// `cn1_windows_hardware.cpp` answer negative Linux errno values -- and +/// replaces the parts that name Linux device nodes: +/// +/// - Serial ports are `COM1`, `COM2`, ... on every Windows. +/// - GPIO, I2C, SPI and PWM go through `Windows.Devices.*`, which answer on a +/// Windows IoT Enterprise board whose BSP ships the bus drivers; an ordinary +/// PC lists none. There is one GPIO controller, listed as `gpio0`. +/// - The backlight is DDC/CI or the integrated panel's WMI brightness, on a +/// 0..100 scale for the latter. +/// - CAN and the watchdog are vendor SDKs on Windows and are not offered. +class WindowsHardware extends KernelHardware { + + @Override + protected int getSupportedFeatures() { + return FEATURE_GPIO | FEATURE_I2C | FEATURE_SPI | FEATURE_SERIAL | FEATURE_PWM | FEATURE_BACKLIGHT; + } + + /// Listeners run on the event dispatch thread. + @Override + protected void deliver(Runnable callback) { + com.codename1.ui.Display.getInstance().callSerially(callback); + } + + @Override + public String[] listSerialPorts() { + return lines(rawList(3)); + } + + @Override + public String[] listSpiDevices() { + // "spi-N" per bus; the chip select is chosen when a device is opened. + return lines(rawList(2)); + } + + @Override + public String[] listCanInterfaces() { + return new String[0]; + } + + private static String[] lines(String s) { + if (s == null || s.length() == 0) { + return new String[0]; + } + java.util.ArrayList out = new java.util.ArrayList(); + int start = 0; + int len = s.length(); + for (int i = 0; i <= len; i++) { + if (i == len || s.charAt(i) == '\n') { + if (i > start) { + out.add(s.substring(start, i)); + } + start = i + 1; + } + } + return out.toArray(new String[out.size()]); + } + + @Override + protected String rawList(int kind) { + return WindowsNative.hwList(kind); + } + + @Override + protected int rawBacklight(int op, int value) { + return WindowsNative.hwBacklight(op, value); + } + + @Override + protected int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges) { + return WindowsNative.hwGpioOpen(chip, line, output, high, bias, edges); + } + + @Override + protected int rawGpioRead(int handle) { + return WindowsNative.hwGpioRead(handle); + } + + @Override + protected int rawGpioWrite(int handle, boolean high) { + return WindowsNative.hwGpioWrite(handle, high); + } + + @Override + protected int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) { + return WindowsNative.hwGpioWaitEdge(handle, timeoutMs, timestampOut); + } + + @Override + protected int rawI2cOpen(int bus, int address) { + return WindowsNative.hwI2cOpen(bus, address); + } + + @Override + protected int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) { + return WindowsNative.hwI2cTransfer(handle, write, writeLen, read, readLen); + } + + @Override + protected int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) { + return WindowsNative.hwSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord); + } + + @Override + protected int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len) { + return WindowsNative.hwSpiTransfer(handle, tx, rx, len); + } + + @Override + protected int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (device == null || device.indexOf('\0') >= 0) { + return -22; + } + return WindowsNative.hwSerialOpen(device, baud, dataBits, parity, stopBits); + } + + @Override + protected int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs) { + return WindowsNative.hwSerialRead(handle, buf, off, len, timeoutMs); + } + + @Override + protected int rawSerialWrite(int handle, byte[] buf, int off, int len) { + return WindowsNative.hwSerialWrite(handle, buf, off, len); + } + + @Override + protected int rawPwmOpen(int chip, int channel) { + return WindowsNative.hwPwmOpen(chip, channel); + } + + @Override + protected int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) { + return WindowsNative.hwPwmConfigure(handle, periodNanos, dutyNanos, enabled); + } + + @Override + protected int rawCanOpen(String iface) { + return WindowsNative.hwCanOpen(iface); + } + + @Override + protected int rawCanSend(int handle, int id, boolean extended, byte[] data, int len) { + return WindowsNative.hwCanSend(handle, id, extended, data, len); + } + + @Override + protected int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs) { + return WindowsNative.hwCanReceive(handle, idOut, data, timeoutMs); + } + + @Override + protected int rawWatchdogOpen(int timeoutSeconds) { + return WindowsNative.hwWatchdogOpen(timeoutSeconds); + } + + @Override + protected int rawWatchdogKick(int handle) { + return WindowsNative.hwWatchdogKick(handle); + } + + @Override + protected void rawClose(int handle, boolean disarm) { + WindowsNative.hwClose(handle, disarm); + } +} diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index 786e137b801..90e6def517c 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -2278,6 +2278,13 @@ public boolean isAsyncEditMode() { */ @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { + // The kiosk target edits through the shared on-screen keyboard: a panel + // usually has no physical keyboard, and the system touch keyboard is not + // dependable once Shell Launcher has replaced explorer. + if (Embedded.VALUE) { + com.codename1.impl.EmbeddedTextEditor.edit(cmp, maxSize, constraint, text); + return; + } // Tapping straight from one field into another: finish the first cleanly // before overlaying the new one, so two controls never float at once. if (editPeer != 0) { @@ -4155,4 +4162,30 @@ public String getDatabasePath(String databaseName) { } return resolveDatabasePath(databaseName); } + + /// True on the embedded (kiosk) target. Fixed at build time; the port's JVM + /// unit tests run with no native library, so a missing native reads as "not + /// embedded". Caught as LinkageError: the device class library has no + /// UnsatisfiedLinkError. + private static final class Embedded { + static final boolean VALUE = query(); + + private static boolean query() { + try { + return WindowsNative.isEmbeddedDisplay(); + } catch (LinkageError notLinked) { + return false; + } + } + } + + private WindowsHardware hardware; + + @Override + public com.codename1.hardware.Hardware getHardware() { + if (hardware == null) { + hardware = new WindowsHardware(); + } + return hardware; + } } diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java index fce8f707d42..a9c840953d9 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java @@ -1193,4 +1193,34 @@ public static native void gl3dDrawArrays(long contextPeer, long pipelinePeer, lo public static native byte[] sqlColBlob(long stmtPeer, int col) throws java.io.IOException; public static native double sqlColDouble(long stmtPeer, int col) throws java.io.IOException; public static native long sqlColLong(long stmtPeer, int col) throws java.io.IOException; + + /// True when the exe was built for the embedded (kiosk) target + /// (`windows.embedded=true`, cn1_windows_embedded.cpp): a constant of the + /// build, never a runtime switch. + public static native boolean isEmbeddedDisplay(); + + // ---- com.codename1.hardware (cn1_windows_hardware.cpp). Each returns a value + // >= 0 or a negative Linux errno, the contract KernelHardware reads; + // -ETIMEDOUT reports a timeout. + public static native int hwGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + public static native int hwGpioRead(int handle); + public static native int hwGpioWrite(int handle, boolean high); + public static native int hwGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + public static native int hwI2cOpen(int bus, int address); + public static native int hwI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + public static native int hwSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + public static native int hwSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + public static native int hwSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + public static native int hwSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + public static native int hwSerialWrite(int handle, byte[] buf, int off, int len); + public static native int hwPwmOpen(int chip, int channel); + public static native int hwPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + public static native int hwCanOpen(String iface); + public static native int hwCanSend(int handle, int id, boolean extended, byte[] data, int len); + public static native int hwCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + public static native int hwWatchdogOpen(int timeoutSeconds); + public static native int hwWatchdogKick(int handle); + public static native void hwClose(int handle, boolean disarm); + public static native String hwList(int kind); + public static native int hwBacklight(int op, int value); } diff --git a/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh b/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh new file mode 100644 index 00000000000..bb6b79a5046 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh @@ -0,0 +1,17 @@ +// Generated from docs/developer-guide/Embedded-Windows.asciidoc source blocks. Edit the guide snippets here, not inline. + +// tag::embedded-windows-properties-001[] +codename1.arg.windows.embedded=true +codename1.arg.windows.embedded.board=imx8 +codename1.arg.windows.embedded.rotation=90 +// end::embedded-windows-properties-001[] + +// tag::embedded-windows-bash-001[] +mvn cn1:deploy-device -Dcn1.device.host=kiosk.local -Dcn1.device.user=Administrator +gradle cn1DeployDevice -Pcn1.device.host=kiosk.local -Pcn1.device.user=Administrator +// end::embedded-windows-bash-001[] + +// tag::embedded-windows-bash-002[] +mvn cn1:deploy-device -Dcn1.device.host=kiosk.local -Dcn1.device.user=Administrator \ + -Dcn1.device.kioskUser=kiosk -Dcn1.device.keyboardFilter=true +// end::embedded-windows-bash-002[] diff --git a/docs/developer-guide/Embedded-Windows.asciidoc b/docs/developer-guide/Embedded-Windows.asciidoc new file mode 100644 index 00000000000..5503e28a738 --- /dev/null +++ b/docs/developer-guide/Embedded-Windows.asciidoc @@ -0,0 +1,130 @@ +== Embedded Windows devices + +Panel PCs, point-of-sale terminals, self-service kiosks and arm64 IoT boards +running *Windows 10/11 IoT Enterprise* run Codename One as the same native `.exe` +the <<_working_with_the_native_windows_port,native Windows port>> builds. No JVM +and no runtime installer go onto the device. Windows still owns the display, the +input and the boot, so the embedded build doesn't change how the application draws. +It changes what surrounds it: the window fills a screen and keeps it, the shell is +the application, and the boards' buses are reachable from Java. + +Windows 10 IoT Core isn't a target. It runs UWP applications only and is no +longer serviced. + +=== Building for a device + +Two build hints turn a native Windows build into an embedded one: + +[source,properties] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-properties-001,indent=0] +---- + +.Embedded Windows build hints +|=== +|Name |Description + +|windows.embedded +|`true`/`false` (default `false`). Builds the kiosk variant described here. Can't +be combined with `windows.xbox`. + +|windows.embedded.board +|The device profile: `generic-x64` (default, a touch panel PC), `generic-x64-mouse`, +`generic-arm64`, `imx8` or `qualcomm-iot`. The profile sets the defaults below and +the CPU architecture, so an arm64 board needs no `windows.arch`. + +|windows.embedded.rotation +|`0`, `90`, `180` or `270`. The display is rotated when the application starts. + +|windows.embedded.dpi +|The density the UI is laid out for. `0` (default) asks Windows. + +|windows.embedded.monitor +|Which monitor the application takes, counting from `0`. + +|windows.embedded.cursor +|`true` shows the mouse pointer. Touch profiles hide it. +|=== + +Each value can be changed on the device without a rebuild, through the matching +environment variable: `CN1_ROTATION`, `CN1_DPI`, `CN1_MONITOR` and `CN1_CURSOR` +(`true` or `1` shows the pointer, `false` or `0` hides it). + +=== What the embedded build does + +* The window has no frame, covers the chosen monitor and stays above every other + window. +* The Windows key, Alt+Tab, Alt+Esc, Alt+F4 and Ctrl+Esc never reach the system + while the application has the screen. Ctrl+Alt+Del is handled by Windows before + any application sees it; the Keyboard Filter below blocks it. +* Text is entered through Codename One's own on-screen keyboard, the one the + <<_embedded_linux_devices,embedded Linux>> build uses, rather than the Windows + touch keyboard, which is unreliable when the application is the shell. +* Standard output and standard error go to the file `CN1_LOG_FILE` names, so a + device with no console still keeps a log. + +=== Deploying to a device + +`deploy-device` installs the application over SSH. Windows ships OpenSSH Server as +an optional feature; enable it on the device and log in once as an administrator +so its key is accepted: + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-bash-001,indent=0] +---- + +The goal recognizes a Windows executable, checks that its architecture matches +the device and copies it to `C:\ProgramData\CodenameOne`. It then makes the +application the device's shell with *Shell Launcher*: it enables the feature +if it's missing, restarts the application whenever it exits, and keeps Explorer +as the shell for administrators, so the device can still be serviced. Enabling +the feature takes a restart, which the goal performs. + +Options, as `cn1.device.*` properties: + +* `kioskUser` limits the kiosk shell to one account, `kiosk` for instance; other + users keep Explorer. +* `keyboardFilter=true` enables the Keyboard Filter and blocks Ctrl+Alt+Del, + Ctrl+Shift+Esc, Win+L, the Windows key, Alt+Tab, Ctrl+Esc and Alt+F4. +* `kioskMode=autostart` replaces no shell and starts the application at logon + instead, for every account, administrators included. Use it on a development + device, or on an edition without Shell Launcher. +* `env` sets machine environment variables, `CN1_ROTATION=90` for instance. Windows + reads them at logon, so they apply from the next one. + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-bash-002,indent=0] +---- + +`cn1:device-logs` follows the application's log file over the same connection. +As on Linux, the application runs with `CN1_PROFILE=device`, so a backend module's +`@Profile("device")` beans are active on the device only. + +Assigned Access, the single-app kiosk of the Pro edition, isn't offered: it runs +only packaged applications, under a restricted account that can't reach the +hardware below. + +=== Hardware access + +`com.codename1.hardware` works on Windows through the device APIs Windows provides: + +|=== +|API |On Windows + +|Serial ports +|Every `COM` port. + +|GPIO, I2C, SPI, PWM +|The `Windows.Devices` APIs. They need a board whose vendor ships the bus drivers, +which IoT boards do and most x64 panel PCs don't. Without one, the feature reports +that it isn't available. I2C and SPI bus `N` is the board's `N`th controller. + +|Backlight +|The monitor's brightness over DDC/CI, or the built-in panel's through WMI. + +|CAN, watchdog +|Not available: Windows has no standard interface for either. Shell Launcher's +restart takes the place of a supervisor. +|=== diff --git a/docs/developer-guide/Working-With-Windows.asciidoc b/docs/developer-guide/Working-With-Windows.asciidoc index 10dcc8e3436..8d47f6573b9 100644 --- a/docs/developer-guide/Working-With-Windows.asciidoc +++ b/docs/developer-guide/Working-With-Windows.asciidoc @@ -113,6 +113,10 @@ and folding identical functions (`/OPT:ICF`). Set `true` to keep debug symbols ( `.pdb` next to the exe) so a native crash address can be symbolized while you are developing. Optimizations stay on in either case. +|windows.embedded +|`true`/`false` (default `false`). Builds a kiosk `.exe` for a Windows IoT Enterprise +device. See <<_embedded_windows_devices>>. + |windows.xbox |Experimental. `true`/`false` (default `false`). Builds a UWP app for an Xbox in Developer Mode instead of a desktop `.exe`, packaged as an `.msix`. See diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index bf9ec0695f9..2c08b8dcacc 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -312,6 +312,8 @@ include::Working-With-Linux.asciidoc[] include::Embedded-Linux.asciidoc[] +include::Embedded-Windows.asciidoc[] + include::Steam.asciidoc[] include::Realtime.asciidoc[] diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java new file mode 100644 index 00000000000..3203f94c66e --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import java.util.LinkedHashMap; +import java.util.Map; + +/// What the embedded Windows (kiosk) target needs to know about a device, +/// keyed by the `windows.embedded.board` build hint. Like the Linux +/// [EmbeddedBoardProfile] a profile only sets defaults: each value has its own +/// hint, and at run time the matching `CN1_*` environment variable overrides +/// it (see `cn1_windows_embedded.cpp`). +/// +/// Windows owns the display driver, so unlike Linux there is no display +/// backend or renderer to choose: the profile is the architecture, the panel +/// density for panels that misreport it, and whether a cursor is shown. +final class WindowsEmbeddedProfile { + final String name; + final String arch; + final int rotation; + final int dpi; + /// True for devices normally driven by a mouse; touch panels hide it. + final boolean cursor; + final String description; + + private static final Map PROFILES = + new LinkedHashMap(); + + static { + add("generic-x64", WindowsNativeBuilder.ARCH_X64, 0, 0, false, + "x86-64 panel PC or kiosk on Windows IoT Enterprise (touch)"); + add("generic-x64-mouse", WindowsNativeBuilder.ARCH_X64, 0, 0, true, + "x86-64 kiosk driven by a mouse or trackball"); + add("generic-arm64", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "Any 64-bit ARM board on Windows IoT Enterprise"); + add("imx8", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "NXP i.MX 8M family with NXP's Windows IoT Enterprise BSP (GPIO, I2C, SPI, PWM)"); + add("qualcomm-iot", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "Qualcomm IoT boards (QCS6490 and kin) on Windows IoT Enterprise"); + } + + private WindowsEmbeddedProfile(String name, String arch, int rotation, int dpi, boolean cursor, + String description) { + this.name = name; + this.arch = arch; + this.rotation = rotation; + this.dpi = dpi; + this.cursor = cursor; + this.description = description; + } + + private static void add(String name, String arch, int rotation, int dpi, boolean cursor, String description) { + PROFILES.put(name, new WindowsEmbeddedProfile(name, arch, rotation, dpi, cursor, description)); + } + + /// The profile for a board name, or `generic-x64` for an unknown or absent + /// one: most Windows kiosks are x86-64 panel PCs. + static WindowsEmbeddedProfile forBoard(String board) { + WindowsEmbeddedProfile p = board == null ? null : PROFILES.get(board.trim()); + return p != null ? p : PROFILES.get("generic-x64"); + } + + static Map all() { + return PROFILES; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java index eb463935cfc..5f535740010 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java @@ -175,9 +175,50 @@ protected String hardeningPlatform(BuildRequest request) { return "win"; } + /// True for `windows.embedded=true`: the kiosk target, one borderless window + /// over a whole monitor of a Windows IoT Enterprise device, normally run as + /// its shell (see `cn1_windows_embedded.cpp` and `cn1:deploy-device`). + static boolean isEmbeddedRequested(BuildRequest request) { + return "true".equalsIgnoreCase(request.getArg("windows.embedded", "false")); + } + + /// The `CN1_DEFAULT_*` defines the kiosk unit reads when the device sets no + /// `CN1_*` environment variable: the board profile's, overridden by the + /// individual hints. Semicolon separated, as CMake lists are. + static String embeddedDefines(BuildRequest request, WindowsEmbeddedProfile profile) { + int rotation = parseIntOr(request.getArg("windows.embedded.rotation", null), profile.rotation); + if (rotation != 90 && rotation != 180 && rotation != 270) { + rotation = 0; + } + int dpi = Math.max(0, parseIntOr(request.getArg("windows.embedded.dpi", null), profile.dpi)); + int monitor = Math.max(0, parseIntOr(request.getArg("windows.embedded.monitor", null), 0)); + String cursorHint = request.getArg("windows.embedded.cursor", null); + boolean cursor = cursorHint == null || cursorHint.trim().isEmpty() + ? profile.cursor : "true".equalsIgnoreCase(cursorHint.trim()); + return "CN1_DEFAULT_ROTATION=" + rotation + ";CN1_DEFAULT_DPI=" + dpi + + ";CN1_DEFAULT_MONITOR=" + monitor + ";CN1_DEFAULT_CURSOR=" + (cursor ? 1 : 0); + } + + private static int parseIntOr(String value, int def) { + if (value == null || value.trim().isEmpty()) { + return def; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ex) { + return def; + } + } + @Override public boolean build(File sourceZip, BuildRequest request) throws BuildException { - String arch = normalizeArch(request.getArg("windows.arch", ARCH_X64)); + boolean embedded = isEmbeddedRequested(request); + WindowsEmbeddedProfile board = WindowsEmbeddedProfile.forBoard(request.getArg("windows.embedded.board", null)); + // A board implies its architecture; windows.arch still wins when given. + String arch = normalizeArch(request.getArg("windows.arch", embedded ? board.arch : ARCH_X64)); + if (embedded) { + log("Embedded Windows (kiosk) target, board profile " + board.name + " (" + board.description + ")"); + } log("Building native Windows app for arch=" + arch + " (triple " + targetTriple(arch) + ")"); File tmpFile = getBuildDirectory(); @@ -451,6 +492,12 @@ public void usesClassMethod(String cls, String method) { + "desktop package with a Widgets Board provider. Build them separately."); } + if (xbox && embedded) { + throw new BuildException("windows.xbox=true and windows.embedded=true cannot be combined: " + + "the kiosk target is a full-trust Win32 shell for Windows IoT Enterprise, the Xbox " + + "package a UWP app container."); + } + List configure = new ArrayList(); configure.add("cmake"); configure.add("-S"); @@ -463,6 +510,10 @@ public void usesClassMethod(String cls, String method) { configure.add("-DCMAKE_C_COMPILER=" + clangClExecutable()); configure.add("-DCMAKE_CXX_COMPILER=" + clangClExecutable()); configure.add("-DCN1_WINDOWS_UWP=" + (xbox ? "ON" : "OFF")); + configure.add("-DCN1_WINDOWS_EMBEDDED=" + (embedded ? "ON" : "OFF")); + if (embedded) { + configure.add("-DCN1_WINDOWS_EMBEDDED_DEFINES=" + embeddedDefines(request, board)); + } if (cross) { addCrossCompileConfigure(configure, triple, arch, xwinSysroot, widgetBoardCompileFlags, msix ? widgetBoardLinkFlags(winAppSdk, arch) : ""); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java index 38725acb420..8050a3ccc26 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java @@ -2953,6 +2953,17 @@ private void doWindowsNativeLocalBuild(File tmpProjectDir, Properties props, Fil } if (e.getWindowsExecutable() != null) { getLog().info("Built native Windows executable: " + e.getWindowsExecutable().getAbsolutePath()); + // Also kept where deploy-device looks for it, as the Linux build is: + // a windows.embedded build goes to a kiosk the same way. + File out = new File(host.buildDirectory(), "windows-native"); + out.mkdirs(); + try { + java.nio.file.Files.copy(e.getWindowsExecutable().toPath(), + new File(out, e.getWindowsExecutable().getName()).toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (java.io.IOException ioe) { + throw new BuildExecutionException("Could not copy the Windows executable to " + out, ioe); + } } } catch (com.codename1.builders.BuildException hardeningEx) { throw new BuildExecutionException(hardeningEx.getMessage(), hardeningEx); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index 87d627c9265..0dee74056ce 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -44,17 +44,36 @@ /// restarted, so a redeploy is one command. The unit starts the application on /// boot, restarts it if it exits, and runs it on the console's virtual /// terminal, which is what the embedded display needs to take the screen. +/// +/// A Windows executable (`windows.embedded=true`) goes to a Windows IoT +/// Enterprise device the same way, over the OpenSSH server Windows ships: +/// copied with `scp`, then installed by a PowerShell script run as the deploy +/// user, who must be an administrator. By default it becomes the device's shell +/// through Shell Launcher v2 -- the application replaces explorer for every +/// account but the Administrators group, which keeps explorer so the device can +/// still be serviced, and Shell Launcher restarts the application when it +/// exits. [#kioskMode(String)] `autostart` instead starts it at logon from a +/// scheduled task, with no lockdown. Assigned Access is not offered: it hosts +/// packaged applications, and its Win32 path is a multi-app kiosk profile that +/// keeps the shell. public final class DeviceDeployer { + /// Where a Windows device keeps the application when no directory is given. + static final String WINDOWS_DIR = "C:\\ProgramData\\CodenameOne"; + private static final String LINUX_DIR = "/opt/codenameone"; private final Log log; private File binary; private String host; private String user; private int port = 22; - private String remoteDir = "/opt/codenameone"; + private String remoteDir = LINUX_DIR; private String service; private String environment = ""; private String profile = "device"; private boolean followLogs; + private String kioskMode = "shell"; + private String kioskUser; + private boolean keyboardFilter; + private Boolean windowsTarget; public DeviceDeployer(Log log) { this.log = log; @@ -108,6 +127,37 @@ public DeviceDeployer profile(String profile) { return this; } + /// Windows only: `shell` (the default) installs the application as the + /// device's shell through Shell Launcher; `autostart` starts it at logon + /// and changes nothing else. + public DeviceDeployer kioskMode(String kioskMode) { + this.kioskMode = kioskMode == null || kioskMode.trim().isEmpty() ? "shell" : kioskMode.trim(); + return this; + } + + /// Windows only: the one account whose shell becomes the application. By + /// default it is every account but the Administrators group. + public DeviceDeployer kioskUser(String kioskUser) { + this.kioskUser = kioskUser; + return this; + } + + /// Windows only: also turns on Keyboard Filter and blocks the shortcuts the + /// application cannot filter itself, Ctrl+Alt+Del among them. Off by + /// default: with it on, only Keyboard Filter's breakout key (the left + /// Windows key pressed five times on the welcome screen) leaves the kiosk. + public DeviceDeployer keyboardFilter(boolean keyboardFilter) { + this.keyboardFilter = keyboardFilter; + return this; + } + + /// Whether the device runs Windows, for [#tailLogs()] when there is no + /// binary to tell; null decides from the binary. + public DeviceDeployer windowsTarget(Boolean windowsTarget) { + this.windowsTarget = windowsTarget; + return this; + } + /// Streams the service's journal after the restart, until interrupted. public DeviceDeployer followLogs(boolean followLogs) { this.followLogs = followLogs; @@ -123,20 +173,37 @@ public void tailLogs() throws BuildException { throw new BuildException("Name the service: set cn1.device.service (or build the app, whose" + " executable names it)"); } + if (isWindowsTarget()) { + name = stripExe(name); + String target = checkedWindowsTarget(name); + run(ssh(target, true, windowsLogCommand(name, 100))); + return; + } String target = checkedTarget(name); run(ssh(target, true, "sudo journalctl -f -n 100 -u " + name + ".service")); } + private boolean isWindowsTarget() { + if (windowsTarget != null) { + return windowsTarget.booleanValue(); + } + return binary != null && binary.isFile() && isPe(binary); + } + public void deploy() throws BuildException { if (host == null || host.trim().isEmpty()) { throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); } if (binary == null || !binary.isFile()) { - throw new BuildException("No native Linux binary to deploy" + (binary == null ? "" : " at " + binary) + throw new BuildException("No native Linux or Windows binary to deploy" + (binary == null ? "" : " at " + binary) + ": build the linux-native target locally or set cn1.device.binary"); } + if (isPe(binary)) { + deployWindows(); + return; + } if (!isElf(binary)) { - throw new BuildException(binary + " is not a Linux executable (no ELF header)"); + throw new BuildException(binary + " is neither a Linux executable (no ELF header) nor a Windows one"); } String name = service != null && !service.trim().isEmpty() ? service.trim() : binary.getName(); String target = checkedTarget(name); @@ -238,6 +305,197 @@ String installScript(String name, String remoteBinary, String machines) { + "sudo systemctl restart -- " + name + ".service\n"; } + private static String stripExe(String name) { + return name.regionMatches(true, Math.max(0, name.length() - 4), ".exe", 0, 4) + ? name.substring(0, name.length() - 4) : name; + } + + private String windowsDir() { + return LINUX_DIR.equals(remoteDir) ? WINDOWS_DIR : remoteDir; + } + + /// As [#checkedTarget(String)], for a Windows device: the directory is a + /// drive-letter path. + private String checkedWindowsTarget(String name) throws BuildException { + String dir = windowsDir(); + if (!dir.matches("[A-Za-z]:\\\\[A-Za-z0-9_. \\\\-]*") || dir.contains("..")) { + throw new BuildException("On Windows cn1.device.dir is a drive-letter path of letters, digits, spaces" + + " and _.- (" + dir + ")"); + } + String saved = remoteDir; + remoteDir = LINUX_DIR; + try { + return checkedTarget(name); + } finally { + remoteDir = saved; + } + } + + private void deployWindows() throws BuildException { + String name = stripExe(service != null && !service.trim().isEmpty() ? service.trim() : binary.getName()); + String target = checkedWindowsTarget(name); + if (!"shell".equals(kioskMode) && !"autostart".equals(kioskMode)) { + throw new BuildException("cn1.device.kioskMode is shell or autostart, not " + kioskMode); + } + String u = kioskUser == null ? "" : kioskUser.trim(); + if (!u.matches("([A-Za-z0-9_][A-Za-z0-9_. -]*(\\\\[A-Za-z0-9_][A-Za-z0-9_. -]*)?)?")) { + throw new BuildException("cn1.device.kioskUser is an account name, optionally DOMAIN\\user (" + u + ")"); + } + File script; + try { + script = File.createTempFile("cn1-deploy-", ".ps1"); + java.nio.file.Files.write(script.toPath(), + windowsInstallScript(name, peMachine(binary)).getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new BuildException("Could not write the install script", ex); + } + try { + String staged = "cn1-deploy-" + name; + log.info("Copying " + binary.getName() + " (" + (binary.length() / 1024) + " KB) to " + target + + " and installing it " + ("shell".equals(kioskMode) ? "as the device's shell" : "to start at logon")); + List scp = new ArrayList(Arrays.asList("scp", "-q", "-P", String.valueOf(port), "--", + binary.getAbsolutePath(), target + ":" + staged + ".exe")); + run(scp); + scp.set(scp.size() - 2, script.getAbsolutePath()); + scp.set(scp.size() - 1, target + ":" + staged + ".ps1"); + run(scp); + // The default OpenSSH shell on Windows is cmd; the staged files are in + // the deploy user's profile, which is where both commands start. + run(ssh(target, false, "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " + + staged + ".ps1")); + } finally { + if (!script.delete()) { + script.deleteOnExit(); + } + } + log.info("Deployed " + windowsDir() + "\\" + name + ".exe"); + if (followLogs) { + run(ssh(target, true, windowsLogCommand(name, 50))); + } + } + + private String windowsLogCommand(String name, int lines) { + return "powershell -NoProfile -Command \"Get-Content -Wait -Tail " + lines + " -LiteralPath '" + + windowsDir() + "\\logs\\" + name + ".log'\""; + } + + private static String psQuote(String s) { + return "'" + s.replace("'", "''") + "'"; + } + + /// The PowerShell run on a Windows device, from the deploy user's profile + /// where `scp` left the executable beside it. It refuses an executable the + /// device cannot run, stops the running copy, installs the new one, writes + /// the environment and the log path as machine variables (a shell has no + /// other environment to inherit), and then configures the kiosk. Turning a + /// Windows feature on needs a restart, which the script does itself, once. + String windowsInstallScript(String name, String machine) { + String dir = windowsDir(); + StringBuilder ps = new StringBuilder(); + ps.append("$ErrorActionPreference = 'Stop'\n") + .append("$name = ").append(psQuote(name)).append("\n") + .append("$dir = ").append(psQuote(dir)).append("\n") + .append("$exe = Join-Path $dir \"$name.exe\"\n") + .append("$staged = Join-Path $PSScriptRoot \"cn1-deploy-$name.exe\"\n") + .append("$reboot = $false\n"); + if (machine != null) { + // An x64 build also runs on Windows on ARM, under emulation. + ps.append("$arch = $env:PROCESSOR_ARCHITECTURE\n") + .append("if (-not (").append("ARM64".equals(machine) + ? "$arch -eq 'ARM64'" : "$arch -eq 'AMD64' -or $arch -eq 'ARM64'").append(")) {\n") + .append(" Write-Error \"$name was built for ").append(machine) + .append(", but this device is $arch: set windows.arch or windows.embedded.board\"\n") + .append("}\n"); + } + ps.append("New-Item -ItemType Directory -Force -Path $dir | Out-Null\n") + // The exe is the shell of every non-administrator: only + // Administrators and SYSTEM may change anything beside it. With + // write access a user could replace it, or plant a DLL the exe + // would load from its own directory first, and run code as the + // next user to log on. ProgramData's inherited ACL lets Users + // create files, so inheritance is cut and the grants replaced. + .append("icacls $dir /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F'" + + " '*S-1-5-32-545:(OI)(CI)RX' | Out-Null\n") + // icacls is a native command: Stop does not see its failure. + .append("if ($LASTEXITCODE -ne 0) { throw \"icacls could not lock down $dir\" }\n") + // The kiosk account is not an administrator and writes its log + // here, the one place it can write. + .append("$logs = Join-Path $dir 'logs'\n") + .append("New-Item -ItemType Directory -Force -Path $logs | Out-Null\n") + .append("icacls $logs /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null\n") + .append("if ($LASTEXITCODE -ne 0) { throw \"icacls could not open $logs to Users\" }\n") + .append("Get-Process -Name $name -ErrorAction SilentlyContinue | Stop-Process -Force\n") + .append("Start-Sleep -Milliseconds 300\n") + .append("Move-Item -Force -LiteralPath $staged -Destination $exe\n") + .append("Remove-Item -Force -LiteralPath $PSCommandPath\n") + .append("[Environment]::SetEnvironmentVariable('CN1_LOG_FILE', (Join-Path $logs \"$name.log\"), 'Machine')\n"); + String pairs = environment; + if (!profile.isEmpty() && !(" " + environment.replace(',', ' ')).contains(" CN1_PROFILE=")) { + pairs = environment + " CN1_PROFILE=" + profile; + } + for (String pair : pairs.split("[,\\s]+")) { + int eq = pair.indexOf('='); + if (eq > 0 && pair.substring(0, eq).matches("[A-Za-z_][A-Za-z0-9_]*")) { + ps.append("[Environment]::SetEnvironmentVariable(").append(psQuote(pair.substring(0, eq))) + .append(", ").append(psQuote(pair.substring(eq + 1))).append(", 'Machine')\n"); + } + } + if ("shell".equals(kioskMode)) { + ps.append("if ((Get-WindowsOptionalFeature -Online -FeatureName Client-EmbeddedShellLauncher).State -ne 'Enabled') {\n") + .append(" Enable-WindowsOptionalFeature -Online -All -NoRestart -FeatureName Client-EmbeddedShellLauncher | Out-Null\n") + .append(" $reboot = $true\n") + .append("}\n") + .append("$sl = [wmiclass]'\\\\localhost\\root\\standardcimv2\\embedded:WESL_UserSetting'\n") + // Restart actions: 0 restarts the shell, 3 does nothing. + .append("$sl.SetEnabled($true) | Out-Null\n") + .append("$sl.SetCustomShell('S-1-5-32-544', 'explorer.exe', $null, $null, 3) | Out-Null\n"); + if (kioskUser != null && !kioskUser.trim().isEmpty()) { + ps.append("$sid = (New-Object System.Security.Principal.NTAccount(").append(psQuote(kioskUser.trim())) + .append(")).Translate([System.Security.Principal.SecurityIdentifier]).Value\n") + .append("$sl.SetCustomShell($sid, $exe, $null, $null, 0) | Out-Null\n"); + } else { + ps.append("$sl.SetDefaultShell($exe, 0) | Out-Null\n"); + } + ps.append("Unregister-ScheduledTask -TaskName \"Codename One $name\" -Confirm:$false -ErrorAction SilentlyContinue\n"); + } else { + ps.append("$action = New-ScheduledTaskAction -Execute $exe -WorkingDirectory $dir\n") + .append("$trigger = New-ScheduledTaskTrigger -AtLogOn\n") + .append("$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited\n") + .append("$settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)" + + " -ExecutionTimeLimit 0 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries\n") + .append("Register-ScheduledTask -Force -TaskName \"Codename One $name\" -Action $action -Trigger $trigger" + + " -Principal $principal -Settings $settings | Out-Null\n"); + } + if (keyboardFilter) { + ps.append("if ((Get-WindowsOptionalFeature -Online -FeatureName Client-KeyboardFilter).State -ne 'Enabled') {\n") + .append(" Enable-WindowsOptionalFeature -Online -All -NoRestart -FeatureName Client-KeyboardFilter | Out-Null\n") + .append(" $reboot = $true\n") + .append("} else {\n") + .append(" foreach ($k in 'Ctrl+Alt+Del', 'Ctrl+Shift+Esc', 'Win+L', 'Alt+Tab', 'Ctrl+Esc', 'Alt+F4', 'Windows') {\n") + .append(" $p = Get-CimInstance -Namespace root\\standardcimv2\\embedded -ClassName WEKF_PredefinedKey" + + " | Where-Object { $_.Id -eq $k }\n") + .append(" if ($p) { Set-CimInstance -InputObject $p -Property @{ Enabled = $true } }\n") + .append(" }\n") + .append("}\n"); + } + ps.append("if ($reboot) {\n") + .append(" Write-Output 'A Windows feature was turned on; restarting the device to finish. Deploy again" + + " after it is back.'\n") + .append(" Restart-Computer -Force\n") + .append(" exit 0\n") + .append("}\n"); + if ("shell".equals(kioskMode)) { + // Shell Launcher started the old copy and restarts its shell when it + // exits, so stopping it above relaunches the new one. A device on which + // nobody is logged on yet starts it at the next logon. + ps.append("Write-Output \"$name is the shell; it restarts when it exits\"\n"); + } else { + ps.append("Start-ScheduledTask -TaskName \"Codename One $name\" -ErrorAction SilentlyContinue\n") + .append("Write-Output \"$name starts at logon\"\n"); + } + return ps.toString(); + } + private List ssh(String target, boolean tty, String script) { List cmd = new ArrayList(Arrays.asList("ssh", "-p", String.valueOf(port))); if (tty) { @@ -314,8 +572,37 @@ static boolean isElf(File f) { } } - /// The newest ELF executable directly under one of `dirs`, or `null`: what a - /// local linux-native build left behind. + static boolean isPe(File f) { + byte[] magic = new byte[2]; + try (InputStream in = new FileInputStream(f)) { + return in.read(magic) == 2 && magic[0] == 'M' && magic[1] == 'Z'; + } catch (IOException ex) { + return false; + } + } + + /// `X64` or `ARM64` from a PE header's machine field, else null. + static String peMachine(File f) { + byte[] h = new byte[4096]; + int n; + try (InputStream in = new FileInputStream(f)) { + n = in.read(h); + } catch (IOException ex) { + return null; + } + if (n < 0x40) { + return null; + } + int pe = (h[0x3c] & 0xff) | ((h[0x3d] & 0xff) << 8) | ((h[0x3e] & 0xff) << 16) | ((h[0x3f] & 0xff) << 24); + if (pe < 0 || pe + 6 > n || h[pe] != 'P' || h[pe + 1] != 'E') { + return null; + } + int m = (h[pe + 4] & 0xff) | ((h[pe + 5] & 0xff) << 8); + return m == 0x8664 ? "X64" : m == 0xAA64 ? "ARM64" : null; + } + + /// The newest executable directly under one of `dirs` -- an ELF binary, or a + /// Windows `.exe` -- or `null`: what a local native build left behind. public static File findBinary(File... dirs) { File best = null; for (File dir : dirs) { @@ -324,7 +611,9 @@ public static File findBinary(File... dirs) { continue; } for (File f : files) { - if (f.isFile() && f.canExecute() && isElf(f) && (best == null || f.lastModified() > best.lastModified())) { + boolean exe = f.isFile() && ((f.canExecute() && isElf(f)) + || (f.getName().toLowerCase(java.util.Locale.ROOT).endsWith(".exe") && isPe(f))); + if (exe && (best == null || f.lastModified() > best.lastModified())) { best = f; } } diff --git a/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java b/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java new file mode 100644 index 00000000000..cb8d4d932b3 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The `windows.embedded` hints reach the kiosk unit as `CN1_DEFAULT_*` defines. +class WindowsEmbeddedTest { + + private static BuildRequest request(String board) { + BuildRequest r = new BuildRequest(); + r.putArgument("windows.embedded", "true"); + if (board != null) { + r.putArgument("windows.embedded.board", board); + } + return r; + } + + @Test + void aTouchPanelHidesTheCursor() { + BuildRequest r = request(null); + assertTrue(WindowsNativeBuilder.isEmbeddedRequested(r)); + WindowsEmbeddedProfile p = WindowsEmbeddedProfile.forBoard(null); + assertEquals("generic-x64", p.name); + assertEquals("CN1_DEFAULT_ROTATION=0;CN1_DEFAULT_DPI=0;CN1_DEFAULT_MONITOR=0;CN1_DEFAULT_CURSOR=0", + WindowsNativeBuilder.embeddedDefines(r, p)); + } + + @Test + void anArmBoardImpliesArm64() { + assertEquals(WindowsNativeBuilder.ARCH_ARM64, WindowsEmbeddedProfile.forBoard("imx8").arch); + assertEquals(WindowsNativeBuilder.ARCH_ARM64, WindowsEmbeddedProfile.forBoard("qualcomm-iot").arch); + } + + @Test + void theHintsOverrideTheProfileAndBadValuesFallBack() { + BuildRequest r = request("generic-x64-mouse"); + r.putArgument("windows.embedded.rotation", "90"); + r.putArgument("windows.embedded.dpi", "160"); + r.putArgument("windows.embedded.monitor", "1"); + WindowsEmbeddedProfile p = WindowsEmbeddedProfile.forBoard("generic-x64-mouse"); + assertEquals("CN1_DEFAULT_ROTATION=90;CN1_DEFAULT_DPI=160;CN1_DEFAULT_MONITOR=1;CN1_DEFAULT_CURSOR=1", + WindowsNativeBuilder.embeddedDefines(r, p)); + r.putArgument("windows.embedded.rotation", "45"); + r.putArgument("windows.embedded.cursor", "false"); + r.putArgument("windows.embedded.dpi", "lots"); + assertEquals("CN1_DEFAULT_ROTATION=0;CN1_DEFAULT_DPI=0;CN1_DEFAULT_MONITOR=1;CN1_DEFAULT_CURSOR=0", + WindowsNativeBuilder.embeddedDefines(r, p)); + } + + @Test + void notRequestedByDefault() { + assertFalse(WindowsNativeBuilder.isEmbeddedRequested(new BuildRequest())); + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java new file mode 100644 index 00000000000..4a328f2f903 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The Windows half of [DeviceDeployer]: a kiosk executable installed as a +/// Windows IoT Enterprise device's shell. +class WindowsDeviceDeployerTest { + + @TempDir + File dir; + + /// A minimal PE image: "MZ", e_lfanew at 0x3c, then "PE\0\0" and the machine. + private File pe(String name, int machine) throws IOException { + byte[] b = new byte[0x100]; + b[0] = 'M'; + b[1] = 'Z'; + b[0x3c] = (byte) 0x80; + b[0x80] = 'P'; + b[0x81] = 'E'; + b[0x84] = (byte) (machine & 0xff); + b[0x85] = (byte) (machine >> 8); + File f = new File(dir, name); + try (OutputStream out = new FileOutputStream(f)) { + out.write(b); + } + return f; + } + + @Test + void readsTheArchitectureOfAWindowsExecutable() throws IOException { + assertEquals("X64", DeviceDeployer.peMachine(pe("a.exe", 0x8664))); + assertEquals("ARM64", DeviceDeployer.peMachine(pe("b.exe", 0xAA64))); + assertNull(DeviceDeployer.peMachine(pe("c.exe", 0x14c))); + assertTrue(DeviceDeployer.isPe(pe("d.exe", 0x8664))); + } + + @Test + void findsAWindowsExecutableToo() throws IOException { + File exe = pe("Kiosk.exe", 0x8664); + assertEquals(exe, DeviceDeployer.findBinary(dir)); + File notExe = pe("Kiosk.dll", 0x8664); + assertTrue(notExe.setLastModified(exe.lastModified() + 5000)); + assertEquals(exe, DeviceDeployer.findBinary(dir), "a DLL is not an application"); + } + + @Test + void theShellModeKeepsExplorerForAdministrators() { + String ps = new DeviceDeployer(new NullLog()).environment("CN1_ROTATION=90") + .windowsInstallScript("Kiosk", "ARM64"); + assertTrue(ps.contains("Client-EmbeddedShellLauncher"), ps); + assertTrue(ps.contains("SetCustomShell('S-1-5-32-544', 'explorer.exe'"), ps); + assertTrue(ps.contains("SetDefaultShell($exe, 0)"), ps); + assertTrue(ps.contains("$dir = 'C:\\ProgramData\\CodenameOne'"), ps); + assertTrue(ps.contains("SetEnvironmentVariable('CN1_ROTATION', '90', 'Machine')"), ps); + assertTrue(ps.contains("SetEnvironmentVariable('CN1_PROFILE', 'device', 'Machine')"), ps); + assertTrue(ps.contains("$arch -eq 'ARM64'"), ps); + assertFalse(ps.contains("Client-KeyboardFilter"), ps); + // Users may run the shell but never write beside it (exe swap, DLL plant); + // only the logs folder is theirs. + assertTrue(ps.contains("icacls $dir /inheritance:r /grant:r"), ps); + assertTrue(ps.contains("'*S-1-5-32-545:(OI)(CI)RX'"), ps); + assertFalse(ps.contains("icacls $dir /grant '*S-1-5-32-545:(OI)(CI)M'"), ps); + assertTrue(ps.contains("icacls $logs /grant '*S-1-5-32-545:(OI)(CI)M'"), ps); + assertTrue(ps.contains("(Join-Path $logs \"$name.log\")"), ps); + // The architecture is checked before anything is replaced. + assertTrue(ps.indexOf("PROCESSOR_ARCHITECTURE") < ps.indexOf("Move-Item"), ps); + } + + @Test + void anX64BuildMayGoToWindowsOnArm() { + String ps = new DeviceDeployer(new NullLog()).windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("$arch -eq 'AMD64' -or $arch -eq 'ARM64'"), ps); + } + + @Test + void oneKioskAccountAndTheKeyboardFilter() { + String ps = new DeviceDeployer(new NullLog()).kioskUser("kiosk").keyboardFilter(true) + .windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("NTAccount('kiosk')"), ps); + assertTrue(ps.contains("SetCustomShell($sid, $exe"), ps); + assertFalse(ps.contains("SetDefaultShell"), ps); + assertTrue(ps.contains("'Ctrl+Alt+Del'"), ps); + } + + @Test + void autostartRegistersALogonTaskAndNoShell() { + String ps = new DeviceDeployer(new NullLog()).kioskMode("autostart").windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("New-ScheduledTaskTrigger -AtLogOn"), ps); + assertFalse(ps.contains("WESL_UserSetting"), ps); + } + + @Test + void valuesCannotBreakOutOfTheirQuotes() { + String ps = new DeviceDeployer(new NullLog()).environment("GREETING=it's") + .windowsInstallScript("Kiosk", null); + assertTrue(ps.contains("'GREETING', 'it''s'"), ps); + } + + @Test + void refusesWhatItCannotInstall() throws IOException { + File exe = pe("Kiosk.exe", 0x8664); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .kioskMode("assigned-access")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .kioskUser("a'; Remove-Item C:\\")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .remoteDir("C:\\..\\Windows")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("-oProxyCommand=x").binary(exe)::deploy); + } + + private static final class NullLog implements com.codename1.build.Log { + public boolean isDebugEnabled() { return false; } + public void debug(CharSequence c) { } + public void debug(CharSequence c, Throwable t) { } + public void debug(Throwable t) { } + public boolean isInfoEnabled() { return false; } + public void info(CharSequence c) { } + public void info(CharSequence c, Throwable t) { } + public void info(Throwable t) { } + public boolean isWarnEnabled() { return false; } + public void warn(CharSequence c) { } + public void warn(CharSequence c, Throwable t) { } + public void warn(Throwable t) { } + public boolean isErrorEnabled() { return false; } + public void error(CharSequence c) { } + public void error(CharSequence c, Throwable t) { } + public void error(Throwable t) { } + } +} diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index ce648a7a87d..5ff833def20 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -494,6 +494,60 @@ static void register(List h) { .type(HintType.STRING) .platform("windows")); + h.add(new Hint("windows.embedded") + .group(HintGroup.WINDOWS) + .type(HintType.BOOLEAN) + .def("false") + .platform("windows") + .doc("Native Windows port only. Builds the kiosk target for a Windows IoT Enterprise " + + "device: one borderless, topmost window covering a whole monitor, the Windows key " + + "and the shell's task switching shortcuts filtered, and text edited through an " + + "on-screen keyboard. `cn1:deploy-device` installs it as the device's shell through " + + "Shell Launcher. Can't be combined with `windows.xbox`.")); + + h.add(new Hint("windows.embedded.board") + .group(HintGroup.WINDOWS) + .type(HintType.STRING) + .def("generic-x64") + .platform("windows") + .doc("Board profile for `windows.embedded`, which sets the architecture and the " + + "cursor and density defaults: `generic-x64`, `generic-x64-mouse`, " + + "`generic-arm64`, `imx8` or `qualcomm-iot`.")); + + h.add(new Hint("windows.embedded.rotation") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Clockwise rotation `windows.embedded` applies to its monitor while it runs: " + + "0, 90, 180 or 270. Windows restores the panel's own orientation when the " + + "application exits. `CN1_ROTATION` on the device overrides it.")); + + h.add(new Hint("windows.embedded.dpi") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Panel density for `windows.embedded` when the panel misreports its size; 0 " + + "uses what Windows reports. `CN1_DPI` on the device overrides it.")); + + h.add(new Hint("windows.embedded.monitor") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Which monitor `windows.embedded` covers, in Windows' enumeration order; 0, or a " + + "monitor that doesn't exist, is the primary. `CN1_MONITOR` on the device " + + "overrides it.")); + + h.add(new Hint("windows.embedded.cursor") + .group(HintGroup.WINDOWS) + .type(HintType.BOOLEAN) + .platform("windows") + .doc("Whether `windows.embedded` shows the mouse cursor. Defaults to the board " + + "profile's choice: hidden on touch panels. `CN1_CURSOR` on the device " + + "overrides it.")); + h.add(new Hint("windows.xbox") .group(HintGroup.WINDOWS) .type(HintType.BOOLEAN) diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java index e7a55993492..4ecf2622343 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java @@ -59,7 +59,8 @@ public void deploy() { File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); String binary = deviceProperty("binary"); File bin = binary != null ? getProject().file(binary) - : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native"), + new File(buildDir, "windows-native")); String port = deviceProperty("port"); String dir = deviceProperty("dir"); try { @@ -69,7 +70,9 @@ public void deploy() { .environment(deviceProperty("env")) .profile(deviceProperty("profile") == null ? "device" : deviceProperty("profile")) .followLogs(deviceProperty("logs") == null ? followLogsByDefault - : "true".equals(deviceProperty("logs"))).deploy(); + : "true".equals(deviceProperty("logs"))) + .kioskMode(deviceProperty("kioskMode")).kioskUser(deviceProperty("kioskUser")) + .keyboardFilter("true".equals(deviceProperty("keyboardFilter"))).deploy(); } catch (BuildException ex) { throw new GradleException(ex.getMessage(), ex); } catch (NumberFormatException ex) { diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java index cb2a4801535..5ad3a0953fc 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java @@ -44,7 +44,8 @@ public void tail() { File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); String binary = deviceProperty("binary"); File bin = binary != null ? getProject().file(binary) - : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native"), + new File(buildDir, "windows-native")); String port = deviceProperty("port"); try { new DeviceDeployer(new GradleLog(getLogger())).host(deviceProperty("host")).user(deviceProperty("user")) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java index 34dde3c6ba5..379a0573351 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java @@ -78,15 +78,30 @@ public class DeployDeviceMojo extends AbstractMojo { @Parameter(property = "cn1.device.logs", defaultValue = "false") private boolean logs; + /** Windows devices: {@code shell} (Shell Launcher, the default) or {@code autostart}. */ + @Parameter(property = "cn1.device.kioskMode", defaultValue = "shell") + private String kioskMode; + + /** Windows devices: the one account whose shell becomes the application; every non-administrator by default. */ + @Parameter(property = "cn1.device.kioskUser") + private String kioskUser; + + /** Windows devices: turns on Keyboard Filter and blocks Ctrl+Alt+Del and the other shell shortcuts. */ + @Parameter(property = "cn1.device.keyboardFilter", defaultValue = "false") + private boolean keyboardFilter; + @Parameter(defaultValue = "${project.build.directory}", readonly = true) private File buildDirectory; - /// The newest executable a local Linux build left: `target/linux-native` of this - /// module, or of the `linux` module beside it when the goal runs at the root. + /// The newest executable a local native build left: `target/linux-native` or + /// `target/windows-native` of this module, or of the `linux` or `win` module + /// beside it when the goal runs at the root. static File findBuiltBinary(File buildDirectory) { File root = buildDirectory.getParentFile(); return DeviceDeployer.findBinary(buildDirectory, new File(buildDirectory, "linux-native"), - root == null ? null : new File(root, "linux" + File.separator + "target" + File.separator + "linux-native")); + new File(buildDirectory, "windows-native"), + root == null ? null : new File(root, "linux" + File.separator + "target" + File.separator + "linux-native"), + root == null ? null : new File(root, "win" + File.separator + "target" + File.separator + "windows-native")); } @Override @@ -97,7 +112,8 @@ public void execute() throws MojoExecutionException { } try { new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port).binary(bin) - .remoteDir(remoteDir).service(service).environment(environment).profile(profile).followLogs(logs).deploy(); + .remoteDir(remoteDir).service(service).environment(environment).profile(profile).followLogs(logs) + .kioskMode(kioskMode).kioskUser(kioskUser).keyboardFilter(keyboardFilter).deploy(); } catch (BuildException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java index f002cae6be3..01e95ae5bd8 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java @@ -87,6 +87,7 @@ public class FrameworkChromeNeverWindowsTest extends UITestBase { "com/codename1/ui/validation/Validator.java", "com/codename1/components/FloatingActionButton.java", "com/codename1/components/InfiniteProgress.java", + "com/codename1/impl/EmbeddedTextEditor.java", }; /** Every construction of a dialog that would take the default if left alone. */ diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 773c394cd3b..09a5e56adfa 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -1491,7 +1491,19 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app writer.append(" string(REPLACE \"/MDd\" \"/MTd\" ${CN1_FLAG_VAR} \"${${CN1_FLAG_VAR}}\")\n"); writer.append(" endforeach()\n"); writer.append(" else()\n"); - writer.append(" target_link_libraries(${PROJECT_NAME} d2d1 dwrite dxgi windowscodecs winhttp ws2_32 user32 gdi32 ole32 oleaut32 uuid mf mfplat mfreadwrite mfuuid shell32 comdlg32 crypt32 bcrypt ncrypt winmm runtimeobject dbghelp advapi32 dwmapi)\n"); + writer.append(" target_link_libraries(${PROJECT_NAME} d2d1 dwrite dxgi windowscodecs winhttp ws2_32 user32 gdi32 ole32 oleaut32 uuid mf mfplat mfreadwrite mfuuid shell32 comdlg32 crypt32 bcrypt ncrypt winmm runtimeobject dbghelp advapi32 dwmapi dxva2)\n"); + writer.append(" endif()\n"); + // CN1_WINDOWS_EMBEDDED (WindowsNativeBuilder passes it for + // windows.embedded=true) is the kiosk target: a borderless window + // over a whole monitor, the shell shortcuts filtered, the shared + // on-screen keyboard (cn1_windows_embedded.cpp). The board profile's + // defaults ride in CN1_WINDOWS_EMBEDDED_DEFINES as CN1_DEFAULT_*. + // dxva2, above, is the DDC/CI backlight of cn1_windows_hardware.cpp, + // which a desktop build answers through as well. + writer.append(" option(CN1_WINDOWS_EMBEDDED \"Build the embedded (kiosk) target\" OFF)\n"); + writer.append(" set(CN1_WINDOWS_EMBEDDED_DEFINES \"\" CACHE STRING \"CN1_DEFAULT_* defaults of the kiosk target, semicolon separated\")\n"); + writer.append(" if(CN1_WINDOWS_EMBEDDED AND NOT CN1_WINDOWS_UWP)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_WINDOWS_EMBEDDED=1 ${CN1_WINDOWS_EMBEDDED_DEFINES})\n"); writer.append(" endif()\n"); // BrowserComponent is backed by WebView2 (cn1_windows_browser.cpp), // gated on the SDK being present: when WEBVIEW2_SDK_DIR points at a diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index b9bfc151dd6..941b85f1a9c 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -769,6 +769,29 @@ void crossCompilesWindowsExeWithXwin() throws Exception { Path uwpExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", true); System.out.println("CN1_XWIN_UWP_EXE=" + uwpExe.toAbsolutePath() + " (" + (Files.size(uwpExe) / 1024) + "KB)"); + // --- the kiosk build (windows.embedded=true) for an x64 panel PC and an + // arm64 IoT board: the embedded window code and the WinRT bus natives + // compile only in this variant. The x64 one is what the Windows CI job + // launches to check the kiosk window (CN1_KIOSK_EXE_OUT). --- + Path kioskExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", false, true, false); + System.out.println("CN1_XWIN_KIOSK_EXE=" + kioskExe.toAbsolutePath()); + String kioskOut = System.getenv("CN1_KIOSK_EXE_OUT"); + boolean ci = kioskOut != null && !kioskOut.trim().isEmpty(); + // CI splats aarch64 too, so a missing arm64 build there is a failure. + if (ci || Files.isDirectory(sys.resolve("crt/lib/aarch64"))) { + Path armExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", false, true, true); + System.out.println("CN1_XWIN_KIOSK_ARM64_EXE=" + armExe.toAbsolutePath()); + } else { + System.out.println("No aarch64 libraries in the sysroot; the arm64 kiosk build was skipped"); + } + if (ci) { + Path dest = Paths.get(kioskOut.trim()); + if (dest.getParent() != null) { + Files.createDirectories(dest.getParent()); + } + Files.copy(kioskExe, dest, StandardCopyOption.REPLACE_EXISTING); + } + // --- the UWP smoke app the Windows CI job installs and launches as a real // UWP activation (windows-cross-compile.yml). It writes a marker into its // package LocalState once its first form has painted, which is the only @@ -880,8 +903,16 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN /// which compiles the app API partition and links WindowsApp.lib alone. static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName, boolean uwp) throws Exception { - String target = "x86_64-pc-windows-msvc"; - String libArch = "x86_64"; + return crossBuildDist(cmakeRoot, sys, clangCl, exeName, uwp, false, false); + } + + /// `embedded` builds the kiosk variant `windows.embedded=true` produces + /// (`-DCN1_WINDOWS_EMBEDDED=ON`); `arm64` targets Windows on Arm, which the + /// arm64 IoT boards run. The sysroot must have been splatted for aarch64 too. + static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName, boolean uwp, + boolean embedded, boolean arm64) throws Exception { + String target = arm64 ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"; + String libArch = arm64 ? "aarch64" : "x86_64"; String inc = String.join(" ", "--target=" + target, imsvc(sys.resolve("crt/include")), @@ -903,13 +934,15 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN "-I", sys.resolve("crt/include").toString(), "-I", sys.resolve("sdk/include/ucrt").toString()); String llvmRc = System.getenv().getOrDefault("CN1_LLVM_RC", "llvm-rc"); - Path buildDir = cmakeRoot.resolve(uwp ? "xbuild-uwp" : "xbuild"); + Path buildDir = cmakeRoot.resolve((uwp ? "xbuild-uwp" : embedded ? "xbuild-embedded" : "xbuild") + + (arm64 ? "-arm64" : "")); Files.createDirectories(buildDir); runCommand(Arrays.asList("cmake", "-S", cmakeRoot.toString(), "-B", buildDir.toString(), "-DCN1_WINDOWS_UWP=" + (uwp ? "ON" : "OFF"), + "-DCN1_WINDOWS_EMBEDDED=" + (embedded ? "ON" : "OFF"), "-G", "Ninja", "-DCMAKE_SYSTEM_NAME=Windows", - "-DCMAKE_SYSTEM_PROCESSOR=AMD64", + "-DCMAKE_SYSTEM_PROCESSOR=" + (arm64 ? "ARM64" : "AMD64"), // STATIC_LIBRARY so CMake's compiler-detection try_compile does not // need a full link before the toolchain flags are fully applied. "-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY",