diff --git a/.github/workflows/backend-performance.yml b/.github/workflows/backend-performance.yml new file mode 100644 index 00000000000..4a0b033a18d --- /dev/null +++ b/.github/workflows/backend-performance.yml @@ -0,0 +1,120 @@ +# Holds the Codename One backend to its headless benchmark results. +# +# Builds the bench demo (vm/backend/demo/bench) twice: as shipped, and with the +# allocation census compiled in. The census run counts what each request +# allocates, exactly. The shipped binary then takes turns with Rust's axum and +# Go's net/http on the same two cores of an arm64 runner, through +# scripts/embedded/bench/headless/run.sh, the harness behind the developer +# guide's tables. gate.py judges speed against Rust from the same job and memory +# and allocation absolutely; its docstring says why, and where the limits live. +# +# The job uploads the raw results, the census and charts drawn from this run, so +# the guide's numbers can be refreshed from a CI machine rather than a laptop. +name: Backend performance gate + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - 'vm/backend/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'scripts/embedded/bench/charts.py' + - 'scripts/embedded/bench/headless/**' + - '.github/workflows/backend-performance.yml' + pull_request: + paths: + - 'vm/backend/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'scripts/embedded/bench/charts.py' + - 'scripts/embedded/bench/headless/**' + - '.github/workflows/backend-performance.yml' + +concurrency: + group: backend-performance-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + gate: + # Four cores: the servers get 0-1 and the load generator 2-3. + runs-on: ubuntu-24.04-arm + timeout-minutes: 90 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Go, Rust, oha and the comparison servers; cached, since none of it + # depends on this repository's code. + - name: Build the benchmark image + uses: docker/build-push-action@v6 + with: + context: scripts/embedded/bench/headless + file: scripts/embedded/bench/headless/Containerfile + tags: cn1-headless:ci + load: true + cache-from: type=gha,scope=backend-performance + cache-to: type=gha,mode=max,scope=backend-performance + + - name: Build the bench demo, shipped and with the census + working-directory: vm/backend + env: + JDK_8_HOME: ${{ env.JAVA_HOME }} + CN1_BACKEND_DEMO: demo/bench + CN1_BACKEND_ENGINE: docker + # The bench demo needs neither the shared contract nor demo/common. + CN1_BACKEND_STANDALONE_DEMO: "1" + run: | + mkdir -p "$RUNNER_TEMP/dist" + ./package.sh Bench com.demo glibc-arm64 + cp target/dist/bench-linux-glibc-arm64 "$RUNNER_TEMP/dist/bench" + CN1_BACKEND_CFLAGS="-DCN1_ALLOC_CENSUS -DCN1_HEAP_HISTOGRAM" \ + ./package.sh Bench com.demo glibc-arm64 + cp target/dist/bench-linux-glibc-arm64 "$RUNNER_TEMP/dist/bench-census" + + - name: Count what a request allocates + run: | + mkdir -p "$RUNNER_TEMP/out" + docker run --rm -v "$RUNNER_TEMP/dist:/dist:ro" \ + -v "$GITHUB_WORKSPACE/scripts/embedded/bench/headless/census.sh:/census.sh:ro" \ + cn1-headless:ci /census.sh /dist/bench-census 200000 | tee "$RUNNER_TEMP/out/census.txt" + + # Privileged so run.sh can drop the page cache before each cold start. + - name: Run the comparison + run: | + docker run --rm --privileged -e RUNTIMES="codenameone rust go" \ + -v "$RUNNER_TEMP/dist:/dist:ro" \ + -v "$GITHUB_WORKSPACE/scripts/embedded/bench/headless/run.sh:/bench/run.sh:ro" \ + cn1-headless:ci /bench/run.sh /dist/bench 3 | tee "$RUNNER_TEMP/out/results.txt" + + - name: Draw the charts + run: | + mkdir -p "$RUNNER_TEMP/out/img" + python3 scripts/embedded/bench/charts.py "$RUNNER_TEMP/out/img" "$RUNNER_TEMP/out/results.txt" + + - name: Judge the results + run: | + python3 scripts/embedded/bench/headless/gate.py "$RUNNER_TEMP/out/results.txt" \ + "$(cat "$RUNNER_TEMP/out/census.txt")" "$GITHUB_STEP_SUMMARY" + + - name: Upload the results + if: always() + uses: actions/upload-artifact@v4 + with: + name: backend-performance + path: ${{ runner.temp }}/out + if-no-files-found: ignore diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml new file mode 100644 index 00000000000..f53a68338d5 --- /dev/null +++ b/.github/workflows/embedded-linux.yml @@ -0,0 +1,128 @@ +# Builds and runs the embedded Linux port (CN1_LINUX_EMBEDDED: DRM/KMS, fbdev or +# memory display, evdev input, no GTK) the way scripts/embedded/README.md tells a +# user to: translate scripts/embedded/demo with translate.sh, build it for arm64 +# with build-dist.sh in Containerfile.dev, and run it on the memory display. +# +# check-demo-run.py then fails the job when frame 60 is missing, wrong or blank, +# or when the speed and memory the developer guide reports have slipped. The +# generated stub file is checked against the natives too, since a native added to +# LinuxNative without a stub breaks only the embedded link. +# +# The GPU renderer (CN1_EMBEDDED_GPU, GLES2 through GBM/EGL) runs on Mesa's +# llvmpipe, since the runner has no GPU: gpu-check draws every kind of operation +# both ways and fails on a differing pixel, and the demo is built a second time +# with the GPU renderer, whose frame 60 must match the software run's exactly. +# llvmpipe says nothing about a board's GPU speed; it holds correctness. +name: Embedded Linux + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - 'Ports/LinuxPort/**' + - 'CodenameOne/src/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'maven/linux/**' + - 'scripts/embedded/**' + - '.github/workflows/embedded-linux.yml' + pull_request: + paths: + - 'Ports/LinuxPort/**' + - 'CodenameOne/src/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'maven/linux/**' + - 'scripts/embedded/**' + - '.github/workflows/embedded-linux.yml' + +concurrency: + group: embedded-linux-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + demo: + runs-on: ubuntu-24.04-arm + timeout-minutes: 75 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: The embedded stubs match the natives + run: python3 scripts/embedded/gen-embedded-stubs.py --check + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + + # Retried: Maven Central answers 429 from the runner CDN edge (see + # linux-build-run.yml). This step only builds. + - name: Build core, the Linux port, JavaAPI and the translator + env: + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + bash ../scripts/ci/retry.sh mvn -B -pl core,linux -am install \ + -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase \ + -Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true \ + -Dmaven.repo.local="$GITHUB_WORKSPACE/.m2-repo" + cd .. + bash scripts/ci/retry.sh mvn -B -f vm/pom.xml -pl JavaAPI,ByteCodeTranslator -am package \ + -DskipTests -Dmaven.repo.local="$GITHUB_WORKSPACE/.m2-repo" + + - name: Translate and build the demo for arm64 + run: | + B="$RUNNER_TEMP/demo" + mkdir -p "$B/classes" + javac -source 1.8 -target 1.8 -cp maven/core/target/classes:maven/linux/target/classes \ + -d "$B/classes" scripts/embedded/demo/src/EmbeddedDemo.java + scripts/embedded/translate.sh "$B/classes" EmbeddedDemo "$B/out" + scripts/embedded/build-dist.sh "$B/out" arm64 + + - name: Run the demo on the memory display + run: | + B="$RUNNER_TEMP/demo" + mkdir -p "$B/result" + docker run --rm -v "$B/out/build-arm64:/cn1:ro" -v "$B/result:/result" cn1-embedded-dev:arm64 \ + bash -c 'cd /tmp && CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS=10 \ + CN1_SNAPSHOT=/result/frame60.png timeout 120 /cn1/EmbeddedDemo > /result/demo.log 2>&1' + python3 scripts/embedded/check-demo-run.py "$B/result/demo.log" "$B/result/frame60.png" \ + "$GITHUB_STEP_SUMMARY" + + - name: The GPU renderer draws what the software one draws + run: scripts/embedded/gpu-check/check-gpu.sh arm64 "$RUNNER_TEMP/demo/result/gpu-check" + + # Same translation, rebuilt with the GPU renderer into the same build + # directory; the software run above has already been judged. + - name: Run the demo with the GPU renderer + env: + CN1_CMAKE_ARGS: -DCN1_EMBEDDED_GPU=ON + run: | + B="$RUNNER_TEMP/demo" + scripts/embedded/build-dist.sh "$B/out" arm64 + mkdir -p "$B/result/gpu" + docker run --rm -e LIBGL_ALWAYS_SOFTWARE=1 -v "$B/out/build-arm64:/cn1:ro" \ + -v "$B/result/gpu:/result" cn1-embedded-dev:arm64 \ + bash -c 'cd /tmp && CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS=10 \ + CN1_SNAPSHOT=/result/frame60.png timeout 120 /cn1/EmbeddedDemo > /result/demo.log 2>&1' + grep -q "renderer gpu" "$B/result/gpu/demo.log" \ + || { echo "the GPU build did not render with the GPU"; tail -20 "$B/result/gpu/demo.log"; exit 1; } + python3 scripts/embedded/check-demo-run.py --max-rss-mb 160 \ + --reference "$B/result/frame60.png" "$B/result/gpu/demo.log" "$B/result/gpu/frame60.png" \ + "$GITHUB_STEP_SUMMARY" + + - name: Upload the run + if: always() + uses: actions/upload-artifact@v4 + with: + name: embedded-demo + path: ${{ runner.temp }}/demo/result + if-no-files-found: ignore diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index d612f1a3638..094fd6bd051 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -43,6 +43,9 @@ on: # marker; a class marked outside these directories needs its path here. - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' + - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' + - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' @@ -53,6 +56,7 @@ on: - 'CodenameOne/src/com/codename1/annotations/JsonIgnore.java' - 'CodenameOne/src/com/codename1/annotations/JsonProperty.java' - 'CodenameOne/src/com/codename1/io/gzip/**' + - 'CodenameOne/src/com/codename1/io/outbox/**' - 'CodenameOne/src/com/codename1/migration/**' - 'CodenameOne/src/com/codename1/impl/migration/**' - 'CodenameOne/src/com/codename1/security/Hash.java' @@ -108,6 +112,9 @@ on: # marker; a class marked outside these directories needs its path here. - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' + - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' + - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' @@ -118,6 +125,7 @@ on: - 'CodenameOne/src/com/codename1/annotations/JsonIgnore.java' - 'CodenameOne/src/com/codename1/annotations/JsonProperty.java' - 'CodenameOne/src/com/codename1/io/gzip/**' + - 'CodenameOne/src/com/codename1/io/outbox/**' - 'CodenameOne/src/com/codename1/migration/**' - 'CodenameOne/src/com/codename1/impl/migration/**' - 'CodenameOne/src/com/codename1/security/Hash.java' diff --git a/CodenameOne/src/com/codename1/hardware/CanBus.java b/CodenameOne/src/com/codename1/hardware/CanBus.java new file mode 100644 index 00000000000..9c3c62c3016 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/CanBus.java @@ -0,0 +1,87 @@ +/* + * 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.hardware; + +/// A CAN network interface, opened through [Hardware#openCan(String)]. +@com.codename1.impl.SharedWithBackend +public final class CanBus { + + private final Hardware hw; + private final int handle; + private final String iface; + private boolean closed; + + CanBus(Hardware hw, int handle, String iface) { + this.hw = hw; + this.handle = handle; + this.iface = iface; + } + + /// The interface name. + public String getInterface() { + return iface; + } + + /// Sends one frame. + /// + /// #### Throws + /// + /// - `HardwareException`: when the send fails or the bus is closed + public void send(CanFrame frame) throws HardwareException { + checkOpen(); + hw.canSend(handle, frame.getId(), frame.isExtended(), frame.getData(), frame.getLength()); + } + + /// Waits up to `timeoutMs` for a frame (negative waits indefinitely) and + /// returns it, or `null` on timeout. Call it off the event dispatch thread. + /// + /// #### Throws + /// + /// - `HardwareException`: when the receive fails or the bus is closed + public CanFrame receive(int timeoutMs) throws HardwareException { + checkOpen(); + int[] id = new int[2]; + byte[] data = new byte[8]; + int len = hw.canReceive(handle, id, data, timeoutMs); + if (len < 0) { + return null; + } + byte[] payload = new byte[len]; + System.arraycopy(data, 0, payload, 0, len); + return new CanFrame(id[0], id[1] != 0, payload); + } + + /// Closes the interface. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("CAN interface " + iface + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/CanFrame.java b/CodenameOne/src/com/codename1/hardware/CanFrame.java new file mode 100644 index 00000000000..aa0cace5ffe --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/CanFrame.java @@ -0,0 +1,77 @@ +/* + * 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.hardware; + +/// One classic CAN frame: an identifier and up to eight data bytes. +@com.codename1.impl.SharedWithBackend +public final class CanFrame { + + private final int id; + private final boolean extended; + private final byte[] data; + + /// Creates a frame. + /// + /// #### Parameters + /// + /// - `id`: an 11-bit identifier, or 29-bit when `extended` + /// + /// - `extended`: `true` for a 29-bit identifier + /// + /// - `data`: zero to eight payload bytes; the array is copied + public CanFrame(int id, boolean extended, byte[] data) { + int max = extended ? 0x1fffffff : 0x7ff; + if (id < 0 || id > max) { + throw new IllegalArgumentException("CAN identifier out of range: " + id); + } + if (data == null || data.length > 8) { + throw new IllegalArgumentException("CAN payload must be 0..8 bytes"); + } + this.id = id; + this.extended = extended; + this.data = new byte[data.length]; + System.arraycopy(data, 0, this.data, 0, data.length); + } + + /// The identifier. + public int getId() { + return id; + } + + /// `true` for a 29-bit identifier. + public boolean isExtended() { + return extended; + } + + /// A copy of the payload. + public byte[] getData() { + byte[] out = new byte[data.length]; + System.arraycopy(data, 0, out, 0, data.length); + return out; + } + + /// The payload length. + public int getLength() { + return data.length; + } +} diff --git a/CodenameOne/src/com/codename1/hardware/GpioListener.java b/CodenameOne/src/com/codename1/hardware/GpioListener.java new file mode 100644 index 00000000000..c6ecb423a42 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/GpioListener.java @@ -0,0 +1,44 @@ +/* + * 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.hardware; + +/// Receives edge events from a [GpioPin] that was opened as an input with +/// edge detection. +/// +/// Events are delivered through [Hardware#deliver(Runnable)] (the event dispatch +/// thread on a client port), in the order the kernel reported them. +@com.codename1.impl.SharedWithBackend +public interface GpioListener { + + /// Called once per detected edge. + /// + /// #### Parameters + /// + /// - `pin`: the pin the edge occurred on + /// + /// - `rising`: `true` for a low-to-high transition, `false` for high-to-low + /// + /// - `timestampNanos`: the kernel's monotonic timestamp of the edge, in + /// nanoseconds; `0` when the port has no timestamp + void edgeDetected(GpioPin pin, boolean rising, long timestampNanos); +} diff --git a/CodenameOne/src/com/codename1/hardware/GpioPin.java b/CodenameOne/src/com/codename1/hardware/GpioPin.java new file mode 100644 index 00000000000..e5e187bba01 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/GpioPin.java @@ -0,0 +1,197 @@ +/* + * 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.hardware; + +import java.util.concurrent.atomic.AtomicBoolean; + +/// A claimed GPIO line, opened through [Hardware#openGpioOutput(String, int, boolean)] +/// or [Hardware#openGpioInput(String, int, int, int)]. +/// +/// The line stays claimed until [#close()]; another process cannot drive it in +/// the meantime. +@com.codename1.impl.SharedWithBackend +public final class GpioPin { + + /// Leave the line's bias as the board configured it. + public static final int BIAS_DEFAULT = 0; + /// Disable internal pull resistors. + public static final int BIAS_DISABLED = 1; + /// Enable the internal pull-up resistor. + public static final int BIAS_PULL_UP = 2; + /// Enable the internal pull-down resistor. + public static final int BIAS_PULL_DOWN = 3; + + /// Report no edges. + public static final int EDGE_NONE = 0; + /// Report low-to-high transitions. + public static final int EDGE_RISING = 1; + /// Report high-to-low transitions. + public static final int EDGE_FALLING = 2; + /// Report both transitions. + public static final int EDGE_BOTH = 3; + + private static final int WAIT_SLICE_MS = 250; + + private final Hardware hw; + private final int handle; + private final String chip; + private final int line; + private final boolean output; + private boolean high; + private boolean closed; + private GpioListener listener; + // Read by the edge thread, written on the caller's thread. + private AtomicBoolean edgeThreadRunning; + + GpioPin(Hardware hw, int handle, String chip, int line, boolean output, boolean initialHigh) { + this.hw = hw; + this.handle = handle; + this.chip = chip; + this.line = line; + this.output = output; + this.high = initialHigh; + } + + /// The controller this line belongs to. + public String getChip() { + return chip; + } + + /// The line offset within its controller. + public int getLine() { + return line; + } + + /// `true` when the line was opened as an output. + public boolean isOutput() { + return output; + } + + /// Reads the line level. For an output this is the level it is driven to. + /// + /// #### Throws + /// + /// - `HardwareException`: when the read fails or the pin is closed + public boolean isHigh() throws HardwareException { + checkOpen(); + if (output) { + return high; + } + return hw.gpioRead(handle); + } + + /// Drives an output line. + /// + /// #### Throws + /// + /// - `HardwareException`: when the line is an input, closed, or the write fails + public void setHigh(boolean value) throws HardwareException { + checkOpen(); + if (!output) { + throw new HardwareException("GPIO line " + line + " is an input"); + } + hw.gpioWrite(handle, value); + high = value; + } + + /// Delivers the edges this input was opened for to `l` on the event + /// dispatch thread, or stops delivery when `l` is `null`. Edges are + /// waited for on a background thread owned by this pin. + /// + /// #### Throws + /// + /// - `HardwareException`: when the line is an output or closed + public void setListener(GpioListener l) throws HardwareException { + checkOpen(); + if (output) { + throw new HardwareException("GPIO line " + line + " is an output"); + } + stopEdgeThread(); + listener = l; + if (l != null) { + final AtomicBoolean running = new AtomicBoolean(true); + final GpioListener target = l; + edgeThreadRunning = running; + Thread t = new Thread(new Runnable() { + @Override + public void run() { + edgeLoop(running, target); + } + }, "GPIO-" + chip + "-" + line); + t.start(); + } + } + + /// The listener set with [#setListener(GpioListener)], or `null`. + public GpioListener getListener() { + return listener; + } + + private void edgeLoop(AtomicBoolean running, final GpioListener target) { + long[] ts = new long[1]; + while (running.get()) { + int r; + try { + r = hw.gpioWaitEdge(handle, WAIT_SLICE_MS, ts); + } catch (HardwareException e) { + return; + } + if (r == Hardware.EDGE_TIMEOUT || !running.get()) { + continue; + } + final boolean rising = r == Hardware.EDGE_RESULT_RISING; + final long when = ts[0]; + hw.deliver(new Runnable() { + @Override + public void run() { + target.edgeDetected(GpioPin.this, rising, when); + } + }); + } + } + + private void stopEdgeThread() { + if (edgeThreadRunning != null) { + edgeThreadRunning.set(false); + edgeThreadRunning = null; + } + } + + /// Releases the line. Further calls on this pin throw; closing twice is + /// harmless. + public void close() { + if (closed) { + return; + } + closed = true; + stopEdgeThread(); + listener = null; + hw.close(handle, false); + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("GPIO line " + line + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/Hardware.java b/CodenameOne/src/com/codename1/hardware/Hardware.java new file mode 100644 index 00000000000..2bcdaa5eca5 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/Hardware.java @@ -0,0 +1,438 @@ +/* + * 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.hardware; + + +/// Entry point to the buses and pins of the device the application runs on: +/// GPIO lines, I2C, SPI, serial ports, PWM, CAN, the hardware watchdog and the +/// display backlight. +/// +/// This is the API for boards such as the Raspberry Pi, i.MX, Rockchip and +/// other embedded Linux devices. Every method is available on every platform; +/// a port without the hardware reports it through [#isSupported(int)] and its +/// `open` methods throw [HardwareException], so the same application runs on a +/// phone, a desktop or the simulator. +/// +/// ```java +/// Hardware hw = Hardware.getInstance(); +/// try { +/// if (hw.isSupported(Hardware.FEATURE_GPIO)) { +/// final GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); +/// led.setHigh(true); +/// +/// GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); +/// button.setListener(new GpioListener() { +/// public void edgeDetected(GpioPin pin, boolean rising, long timestampNanos) { +/// try { +/// led.setHigh(!led.isHigh()); +/// } catch (HardwareException e) { +/// Log.e(e); +/// } +/// } +/// }); +/// } +/// } catch (HardwareException e) { +/// Log.e(e); +/// } +/// ``` +/// +/// #### Threading +/// +/// The transfer methods block for the duration of the bus transaction, which +/// is microseconds for GPIO, I2C and SPI. Serial and CAN reads can wait for +/// data; call them off the event dispatch thread, or use their timeout +/// arguments. Listener callbacks always arrive on the event dispatch thread. +/// +/// #### For port implementors +/// +/// A port subclasses this class and overrides the protected primitives, which +/// take and return integer handles. The device classes ([GpioPin], +/// [I2cDevice], ...) are written once over those primitives, so a port never +/// subclasses them. Every primitive's default throws [HardwareException]. +@com.codename1.impl.SharedWithBackend +public class Hardware { + + /// GPIO lines through the GPIO character device. + public static final int FEATURE_GPIO = 1; + /// I2C buses. + public static final int FEATURE_I2C = 2; + /// SPI buses. + public static final int FEATURE_SPI = 4; + /// Serial ports (UART, USB serial adapters). + public static final int FEATURE_SERIAL = 8; + /// Hardware PWM channels. + public static final int FEATURE_PWM = 16; + /// CAN buses. + public static final int FEATURE_CAN = 32; + /// The hardware watchdog. + public static final int FEATURE_WATCHDOG = 64; + /// Display backlight brightness control. + public static final int FEATURE_BACKLIGHT = 128; + + /// Edge-wait result: the wait timed out. + protected static final int EDGE_TIMEOUT = -1; + /// Edge-wait result: a falling edge. + protected static final int EDGE_RESULT_FALLING = 0; + /// Edge-wait result: a rising edge. + protected static final int EDGE_RESULT_RISING = 1; + + private static final Hardware DEFAULT = new Hardware(); + private static Hardware installed; + + /// Ports construct subclasses; applications call [#getInstance()]. + protected Hardware() { + } + + /// Returns the hardware entry point of the current port. On a port without + /// hardware access this is a base instance that supports nothing, so + /// callers never need a `null` check. + public static Hardware getInstance() { + Hardware h = installed; + return h != null ? h : DEFAULT; + } + + /// Makes `hardware` the instance [#getInstance()] returns; `null` restores + /// the unsupported default. `Display` installs the port's instance when it + /// starts, and the backend runtime installs its own, which is why this + /// class never names `Display` itself. + public static void install(Hardware hardware) { + installed = hardware; + } + + /// Hands a listener callback to the thread listeners run on. The client + /// ports post it to the event dispatch thread; the default, used by the + /// headless backend, runs it on the calling thread. + protected void deliver(Runnable callback) { + callback.run(); + } + + /// `true` when the port supports at least one hardware feature. + public boolean isSupported() { + return getSupportedFeatures() != 0; + } + + /// `true` when the port supports the given feature, one of the `FEATURE_` + /// constants. Support means the port can drive the bus; whether the board + /// exposes one is answered by the matching `list` method. + public boolean isSupported(int feature) { + return (getSupportedFeatures() & feature) == feature; + } + + /// Bit mask of the `FEATURE_` constants this port implements. Ports + /// override this; the default supports nothing. + protected int getSupportedFeatures() { + return 0; + } + + // ------------------------------------------------------------ discovery + + /// Names of the GPIO controllers on this device, for example `gpiochip0`. + /// Empty when there are none or GPIO is unsupported. + public String[] listGpioChips() { + return new String[0]; + } + + /// Numbers of the I2C buses on this device (`/dev/i2c-N` on Linux). + public int[] listI2cBuses() { + return new int[0]; + } + + /// SPI devices as `bus.chipSelect` strings, for example `0.1`. + public String[] listSpiDevices() { + return new String[0]; + } + + /// Device paths of the serial ports on this device, for example + /// `/dev/ttyAMA0` or `/dev/ttyUSB0`. + public String[] listSerialPorts() { + return new String[0]; + } + + /// Names of the CAN network interfaces, for example `can0`. + public String[] listCanInterfaces() { + return new String[0]; + } + + // ------------------------------------------------------------ factories + + /// Claims a GPIO line as an output. + /// + /// #### Parameters + /// + /// - `chip`: the controller name from [#listGpioChips()] + /// + /// - `line`: the line offset within the controller (the BCM number on a + /// Raspberry Pi's `gpiochip0`) + /// + /// - `initialHigh`: the level the line is driven to when claimed + /// + /// #### Throws + /// + /// - `HardwareException`: when the line does not exist, is claimed by + /// another consumer, or GPIO is unsupported + public GpioPin openGpioOutput(String chip, int line, boolean initialHigh) throws HardwareException { + int h = gpioOpen(chip, line, true, initialHigh, GpioPin.BIAS_DEFAULT, GpioPin.EDGE_NONE); + return new GpioPin(this, h, chip, line, true, initialHigh); + } + + /// Claims a GPIO line as an input. + /// + /// #### Parameters + /// + /// - `chip`: the controller name from [#listGpioChips()] + /// + /// - `line`: the line offset within the controller + /// + /// - `bias`: one of the `GpioPin.BIAS_` constants + /// + /// - `edges`: which edges [GpioPin#setListener(GpioListener)] reports, one + /// of the `GpioPin.EDGE_` constants + /// + /// #### Throws + /// + /// - `HardwareException`: when the line cannot be claimed + public GpioPin openGpioInput(String chip, int line, int bias, int edges) throws HardwareException { + int h = gpioOpen(chip, line, false, false, bias, edges); + return new GpioPin(this, h, chip, line, false, false); + } + + /// Opens the device at a 7-bit address on an I2C bus. + /// + /// #### Throws + /// + /// - `HardwareException`: when the bus does not exist or I2C is unsupported + public I2cDevice openI2c(int bus, int address) throws HardwareException { + if (address < 0 || address > 0x7f) { + throw new HardwareException("I2C address out of range: " + address); + } + return new I2cDevice(this, i2cOpen(bus, address), bus, address); + } + + /// Opens an SPI device. + /// + /// #### Parameters + /// + /// - `bus`: the SPI controller number + /// + /// - `chipSelect`: the chip-select line + /// + /// - `mode`: SPI mode 0 to 3 (clock polarity and phase) + /// + /// - `speedHz`: the clock rate + /// + /// - `bitsPerWord`: the word size, usually 8 + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not exist or rejects the + /// configuration + public SpiDevice openSpi(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + if (mode < 0 || mode > 3) { + throw new HardwareException("SPI mode must be 0..3: " + mode); + } + return new SpiDevice(this, spiOpen(bus, chipSelect, mode, speedHz, bitsPerWord), bus, chipSelect); + } + + /// Opens a serial port in raw mode. + /// + /// #### Parameters + /// + /// - `device`: the port, from [#listSerialPorts()] + /// + /// - `baud`: the baud rate, for example 9600 or 115200 + /// + /// - `dataBits`: 5 to 8 + /// + /// - `parity`: one of the `SerialPort.PARITY_` constants + /// + /// - `stopBits`: 1 or 2 + /// + /// #### Throws + /// + /// - `HardwareException`: when the port cannot be opened or the rate is + /// not supported + public SerialPort openSerial(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + return new SerialPort(this, serialOpen(device, baud, dataBits, parity, stopBits), device); + } + + /// Opens a hardware PWM channel. The channel starts disabled. + /// + /// #### Throws + /// + /// - `HardwareException`: when the channel does not exist + public PwmChannel openPwm(int chip, int channel) throws HardwareException { + return new PwmChannel(this, pwmOpen(chip, channel), chip, channel); + } + + /// Opens a CAN network interface. The interface must already be up and + /// configured with its bit rate (`ip link set can0 up type can bitrate + /// 500000`), which needs privileges an application normally lacks. + /// + /// #### Throws + /// + /// - `HardwareException`: when the interface does not exist + public CanBus openCan(String iface) throws HardwareException { + return new CanBus(this, canOpen(iface), iface); + } + + /// Opens and starts the hardware watchdog. From this call on the device + /// resets unless [Watchdog#kick()] is called within the timeout. + /// + /// #### Throws + /// + /// - `HardwareException`: when there is no watchdog or it is in use + public Watchdog openWatchdog(int timeoutSeconds) throws HardwareException { + return new Watchdog(this, watchdogOpen(timeoutSeconds)); + } + + /// The current backlight brightness, from `0` to + /// [#getMaxBacklightBrightness()], or `-1` when not supported. + public int getBacklightBrightness() { + return -1; + } + + /// The highest backlight brightness value, or `-1` when not supported. + public int getMaxBacklightBrightness() { + return -1; + } + + /// Sets the backlight brightness, clamped to the valid range. + /// + /// #### Throws + /// + /// - `HardwareException`: when not supported or not permitted + public void setBacklightBrightness(int value) throws HardwareException { + throw unsupported("backlight"); + } + + // ----------------------------------------------------------- primitives + + /// Creates the exception the default primitives throw. + protected static HardwareException unsupported(String what) { + return new HardwareException(what + " is not supported on this platform"); + } + + /// Claims a GPIO line and returns its handle. + protected int gpioOpen(String chip, int line, boolean output, boolean initialHigh, int bias, int edges) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Reads a GPIO line: `true` for high. + protected boolean gpioRead(int handle) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Drives an output GPIO line. + protected void gpioWrite(int handle, boolean high) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Waits up to `timeoutMs` for an edge on an input line. Returns one of + /// [#EDGE_TIMEOUT], [#EDGE_RESULT_FALLING] or [#EDGE_RESULT_RISING] and + /// stores the edge's timestamp in nanoseconds in `timestampOut[0]`. + /// Called from a background thread, never the event dispatch thread. + protected int gpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Opens an I2C device and returns its handle. + protected int i2cOpen(int bus, int address) throws HardwareException { + throw unsupported("I2C"); + } + + /// Performs one combined I2C transaction: writes `writeLen` bytes from + /// `write`, then, with a repeated start and no stop between, reads + /// `readLen` bytes into `read`. Either length may be zero. + protected void i2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) throws HardwareException { + throw unsupported("I2C"); + } + + /// Opens and configures an SPI device and returns its handle. + protected int spiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + throw unsupported("SPI"); + } + + /// Performs one full-duplex SPI transfer of `len` bytes. `rx` may be the + /// same array as `tx`. + protected void spiTransfer(int handle, byte[] tx, byte[] rx, int len) throws HardwareException { + throw unsupported("SPI"); + } + + /// Opens and configures a serial port and returns its handle. + protected int serialOpen(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + throw unsupported("Serial"); + } + + /// Reads up to `len` bytes, waiting at most `timeoutMs` (negative waits + /// forever) for the first one. Returns the count read, `0` on timeout. + protected int serialRead(int handle, byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + throw unsupported("Serial"); + } + + /// Writes all `len` bytes. + protected void serialWrite(int handle, byte[] buf, int off, int len) throws HardwareException { + throw unsupported("Serial"); + } + + /// Opens a PWM channel and returns its handle. + protected int pwmOpen(int chip, int channel) throws HardwareException { + throw unsupported("PWM"); + } + + /// Configures a PWM channel. Duty may not exceed the period. + protected void pwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + throw unsupported("PWM"); + } + + /// Opens a CAN interface and returns its handle. + protected int canOpen(String iface) throws HardwareException { + throw unsupported("CAN"); + } + + /// Sends one CAN frame. + protected void canSend(int handle, int id, boolean extended, byte[] data, int len) throws HardwareException { + throw unsupported("CAN"); + } + + /// Waits up to `timeoutMs` for a CAN frame. Returns the payload length, + /// or `-1` on timeout; the identifier goes into `idOut[0]` and `idOut[1]` + /// is `1` for an extended identifier. + protected int canReceive(int handle, int[] idOut, byte[] data, int timeoutMs) throws HardwareException { + throw unsupported("CAN"); + } + + /// Opens the watchdog with a timeout and returns its handle. + protected int watchdogOpen(int timeoutSeconds) throws HardwareException { + throw unsupported("Watchdog"); + } + + /// Resets the watchdog timer. + protected void watchdogKick(int handle) throws HardwareException { + throw unsupported("Watchdog"); + } + + /// Closes any handle. `disarm` applies to a watchdog only: `true` stops it + /// where the driver allows, `false` leaves it running so the device resets. + protected void close(int handle, boolean disarm) { + } +} diff --git a/CodenameOne/src/com/codename1/hardware/HardwareException.java b/CodenameOne/src/com/codename1/hardware/HardwareException.java new file mode 100644 index 00000000000..6c1e053da4c --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/HardwareException.java @@ -0,0 +1,56 @@ +/* + * 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.hardware; + +import java.io.IOException; + +/// Thrown when a hardware bus or pin cannot be opened or an I/O operation on +/// it fails: the device node is absent, the process lacks permission for it, +/// the line is already claimed, or the peripheral did not acknowledge. +/// +/// [#getErrno()] carries the operating system error number when the port has +/// one (Linux `errno`), and `0` otherwise, so code can tell "no such device" +/// from "permission denied" without parsing the message. +@com.codename1.impl.SharedWithBackend +public class HardwareException extends IOException { + + private final int errno; + + /// Creates an exception with a message and no operating system error. + public HardwareException(String message) { + this(message, 0); + } + + /// Creates an exception with a message and the operating system error + /// number that caused it. + public HardwareException(String message, int errno) { + super(message); + this.errno = errno; + } + + /// The operating system error number behind this failure, or `0` when the + /// port did not report one. + public int getErrno() { + return errno; + } +} diff --git a/CodenameOne/src/com/codename1/hardware/I2cDevice.java b/CodenameOne/src/com/codename1/hardware/I2cDevice.java new file mode 100644 index 00000000000..53372c6ecc3 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/I2cDevice.java @@ -0,0 +1,135 @@ +/* + * 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.hardware; + +/// A device on an I2C bus, opened through [Hardware#openI2c(int, int)]. +/// +/// The register helpers cover the common sensor convention: write the register +/// address, then read with a repeated start. +/// +/// ```java +/// I2cDevice bme280 = Hardware.getInstance().openI2c(1, 0x76); +/// int chipId = bme280.readRegister(0xd0); // 0x60 on a BME280 +/// ``` +@com.codename1.impl.SharedWithBackend +public final class I2cDevice { + + private final Hardware hw; + private final int handle; + private final int bus; + private final int address; + private boolean closed; + + I2cDevice(Hardware hw, int handle, int bus, int address) { + this.hw = hw; + this.handle = handle; + this.bus = bus; + this.address = address; + } + + /// The bus number. + public int getBus() { + return bus; + } + + /// The 7-bit device address. + public int getAddress() { + return address; + } + + /// Writes `data` in one transaction. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public void write(byte[] data) throws HardwareException { + checkOpen(); + hw.i2cTransfer(handle, data, data.length, null, 0); + } + + /// Reads `len` bytes in one transaction. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] read(int len) throws HardwareException { + checkOpen(); + byte[] out = new byte[len]; + hw.i2cTransfer(handle, null, 0, out, len); + return out; + } + + /// Writes `write`, then reads `readLen` bytes after a repeated start, as a + /// single transaction with no stop condition between them. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] writeRead(byte[] write, int readLen) throws HardwareException { + checkOpen(); + byte[] out = new byte[readLen]; + hw.i2cTransfer(handle, write, write.length, out, readLen); + return out; + } + + /// Reads one 8-bit register. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public int readRegister(int register) throws HardwareException { + return writeRead(new byte[] {(byte) register}, 1)[0] & 0xff; + } + + /// Reads `len` consecutive registers starting at `register`. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] readRegisters(int register, int len) throws HardwareException { + return writeRead(new byte[] {(byte) register}, len); + } + + /// Writes one 8-bit register. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public void writeRegister(int register, int value) throws HardwareException { + write(new byte[] {(byte) register, (byte) value}); + } + + /// Releases the device. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("I2C device 0x" + Integer.toHexString(address) + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/PwmChannel.java b/CodenameOne/src/com/codename1/hardware/PwmChannel.java new file mode 100644 index 00000000000..5d441540f0a --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/PwmChannel.java @@ -0,0 +1,122 @@ +/* + * 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.hardware; + +/// A hardware PWM channel, opened through [Hardware#openPwm(int, int)]. +/// +/// ```java +/// PwmChannel servo = Hardware.getInstance().openPwm(0, 0); +/// servo.configure(20000000L, 1500000L, true); // 50 Hz, 1.5 ms pulse: centred +/// ``` +@com.codename1.impl.SharedWithBackend +public final class PwmChannel { + + private final Hardware hw; + private final int handle; + private final int chip; + private final int channel; + private long periodNanos; + private long dutyNanos; + private boolean enabled; + private boolean closed; + + PwmChannel(Hardware hw, int handle, int chip, int channel) { + this.hw = hw; + this.handle = handle; + this.chip = chip; + this.channel = channel; + } + + /// The PWM controller number. + public int getChip() { + return chip; + } + + /// The channel within the controller. + public int getChannel() { + return channel; + } + + /// Sets the period, the high time and whether the output runs. + /// + /// #### Throws + /// + /// - `HardwareException`: when the duty exceeds the period, the + /// controller rejects the values, or the channel is closed + public void configure(long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + if (closed) { + throw new HardwareException("PWM channel " + chip + "/" + channel + " is closed"); + } + if (periodNanos <= 0 || dutyNanos < 0 || dutyNanos > periodNanos) { + throw new HardwareException("PWM duty must be within 0..period"); + } + hw.pwmConfigure(handle, periodNanos, dutyNanos, enabled); + this.periodNanos = periodNanos; + this.dutyNanos = dutyNanos; + this.enabled = enabled; + } + + /// Sets the duty cycle as a fraction of the current period, keeping the + /// period and enabled state. + /// + /// #### Throws + /// + /// - `HardwareException`: when no period was configured yet + public void setDutyCycle(double fraction) throws HardwareException { + if (periodNanos <= 0) { + throw new HardwareException("Configure a PWM period first"); + } + double f = fraction < 0 ? 0 : (fraction > 1 ? 1 : fraction); + configure(periodNanos, (long) (periodNanos * f), enabled); + } + + /// The configured period in nanoseconds, `0` before [#configure(long, long, boolean)]. + public long getPeriodNanos() { + return periodNanos; + } + + /// The configured high time in nanoseconds. + public long getDutyNanos() { + return dutyNanos; + } + + /// `true` while the output runs. + public boolean isEnabled() { + return enabled; + } + + /// Disables the output and releases the channel. Closing twice is harmless. + public void close() { + if (!closed) { + if (enabled) { + try { + hw.pwmConfigure(handle, periodNanos, dutyNanos, false); + } catch (HardwareException ignored) { + // Releasing is what matters; the controller may already be gone. + } + } + closed = true; + hw.close(handle, false); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/SerialPort.java b/CodenameOne/src/com/codename1/hardware/SerialPort.java new file mode 100644 index 00000000000..7cecb5e6fe1 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/SerialPort.java @@ -0,0 +1,144 @@ +/* + * 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.hardware; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/// A serial port in raw mode, opened through +/// [Hardware#openSerial(String, int, int, int, int)]. +/// +/// Reads block; call them off the event dispatch thread, or use +/// [#read(byte[], int, int, int)] with a timeout. +@com.codename1.impl.SharedWithBackend +public final class SerialPort { + + /// No parity bit. + public static final int PARITY_NONE = 0; + /// Odd parity. + public static final int PARITY_ODD = 1; + /// Even parity. + public static final int PARITY_EVEN = 2; + + private final Hardware hw; + private final int handle; + private final String device; + private boolean closed; + private InputStream in; + private OutputStream out; + + SerialPort(Hardware hw, int handle, String device) { + this.hw = hw; + this.handle = handle; + this.device = device; + } + + /// The device path this port was opened on. + public String getDevice() { + return device; + } + + /// Reads up to `len` bytes, waiting at most `timeoutMs` for the first + /// (negative waits indefinitely). Returns the number read, `0` on timeout. + /// + /// #### Throws + /// + /// - `HardwareException`: when the read fails or the port is closed + public int read(byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + checkOpen(); + return hw.serialRead(handle, buf, off, len, timeoutMs); + } + + /// Writes all of `buf[off..off+len)`. + /// + /// #### Throws + /// + /// - `HardwareException`: when the write fails or the port is closed + public void write(byte[] buf, int off, int len) throws HardwareException { + checkOpen(); + hw.serialWrite(handle, buf, off, len); + } + + /// A blocking stream over this port. Its `read` waits until data arrives + /// and returns `-1` only after [#close()]. + public InputStream getInputStream() { + if (in == null) { + in = new InputStream() { + @Override + public int read() throws IOException { + byte[] one = new byte[1]; + int n = read(one, 0, 1); + return n <= 0 ? -1 : one[0] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + while (!closed) { + int n = hw.serialRead(handle, b, off, len, 250); + if (n > 0) { + return n; + } + } + return -1; + } + }; + } + return in; + } + + /// A stream that writes to this port. + public OutputStream getOutputStream() { + if (out == null) { + out = new OutputStream() { + @Override + public void write(int b) throws IOException { + write(new byte[] {(byte) b}, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + SerialPort.this.write(b, off, len); + } + }; + } + return out; + } + + /// Closes the port. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("Serial port " + device + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/SpiDevice.java b/CodenameOne/src/com/codename1/hardware/SpiDevice.java new file mode 100644 index 00000000000..cc5654f472f --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/SpiDevice.java @@ -0,0 +1,87 @@ +/* + * 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.hardware; + +/// A device on an SPI bus, opened through +/// [Hardware#openSpi(int, int, int, int, int)]. +@com.codename1.impl.SharedWithBackend +public final class SpiDevice { + + private final Hardware hw; + private final int handle; + private final int bus; + private final int chipSelect; + private boolean closed; + + SpiDevice(Hardware hw, int handle, int bus, int chipSelect) { + this.hw = hw; + this.handle = handle; + this.bus = bus; + this.chipSelect = chipSelect; + } + + /// The SPI controller number. + public int getBus() { + return bus; + } + + /// The chip-select line. + public int getChipSelect() { + return chipSelect; + } + + /// Clocks `tx` out and returns the bytes clocked in at the same time. + /// + /// #### Throws + /// + /// - `HardwareException`: when the transfer fails or the device is closed + public byte[] transfer(byte[] tx) throws HardwareException { + byte[] rx = new byte[tx.length]; + transfer(tx, rx); + return rx; + } + + /// Clocks `tx` out while filling `rx`; both must be the same length, and + /// may be the same array. + /// + /// #### Throws + /// + /// - `HardwareException`: when the transfer fails or the device is closed + public void transfer(byte[] tx, byte[] rx) throws HardwareException { + if (closed) { + throw new HardwareException("SPI device " + bus + "." + chipSelect + " is closed"); + } + if (rx.length != tx.length) { + throw new HardwareException("SPI tx and rx lengths differ"); + } + hw.spiTransfer(handle, tx, rx, tx.length); + } + + /// Releases the device. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/Watchdog.java b/CodenameOne/src/com/codename1/hardware/Watchdog.java new file mode 100644 index 00000000000..42c17e3576c --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/Watchdog.java @@ -0,0 +1,81 @@ +/* + * 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.hardware; + +/// The hardware watchdog, opened through [Hardware#openWatchdog(int)]. +/// +/// Once open, the device resets unless [#kick()] is called within the +/// timeout. Kick it from a timer that only runs while the application is +/// healthy, so a hung event dispatch thread causes a reset: +/// +/// ```java +/// try { +/// final Watchdog wd = Hardware.getInstance().openWatchdog(15); +/// UITimer.timer(5000, true, Display.getInstance().getCurrent(), () -> wd.kickQuietly()); +/// } catch (HardwareException e) { +/// Log.e(e); +/// } +/// ``` +@com.codename1.impl.SharedWithBackend +public final class Watchdog { + + private final Hardware hw; + private final int handle; + private boolean closed; + + Watchdog(Hardware hw, int handle) { + this.hw = hw; + this.handle = handle; + } + + /// Restarts the countdown. + /// + /// #### Throws + /// + /// - `HardwareException`: when the watchdog is closed or the kick fails + public void kick() throws HardwareException { + if (closed) { + throw new HardwareException("Watchdog is closed"); + } + hw.watchdogKick(handle); + } + + /// [#kick()], ignoring failures, for use from a timer. + public void kickQuietly() { + try { + kick(); + } catch (HardwareException ignored) { + // A failed kick is what the watchdog exists to catch. + } + } + + /// Closes the watchdog. With `disarm` the timer stops where the driver + /// allows it; without, the device resets when the timeout runs out, which + /// is the right choice on the way down after an unrecoverable error. + public void close(boolean disarm) { + if (!closed) { + closed = true; + hw.close(handle, disarm); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/package-info.java b/CodenameOne/src/com/codename1/hardware/package-info.java new file mode 100644 index 00000000000..0cdd9e62e7d --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/package-info.java @@ -0,0 +1,31 @@ +/* + * 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. + */ +/// Buses and pins of embedded devices: GPIO lines (`GpioPin`, +/// `GpioListener`), I2C (`I2cDevice`), SPI (`SpiDevice`), serial ports +/// (`SerialPort`), PWM (`PwmChannel`), CAN (`CanBus`, `CanFrame`), the +/// hardware watchdog (`Watchdog`) and backlight brightness. +/// +/// `Hardware.getInstance()` is the entry point. Ports without hardware access +/// report every feature as unsupported, so an application written against this +/// package also runs on phones, desktops and the simulator. +package com.codename1.hardware; diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 803e20b250a..b033bef3ef1 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -8696,6 +8696,12 @@ public SecureStorage getSecureStorage() { public com.codename1.nfc.Nfc getNfc() { return null; } + /// Returns the port-specific hardware entry point. Default implementation + /// returns {@code null}; ports with bus and pin access override this to + /// return a cached {@link com.codename1.hardware.Hardware} subclass. + public com.codename1.hardware.Hardware getHardware() { + return null; + } /// Returns the port-specific local calendar source, or null when the port /// has no device calendar integration. @@ -10651,6 +10657,31 @@ public void setInsecure(Object connection, boolean insecure) { } + /// True when [#setClientCertificate(Object, String, String, byte[])] + /// is implemented. + public boolean isClientCertificateSupported() { + return false; + } + + /// Presents a client certificate on this connection's TLS handshake. + /// + /// #### Parameters + /// + /// - `connection`: the connection from `connect` + /// + /// - `certificateChainPem`: one or more PEM certificates, the device's first + /// + /// - `keyAlgorithm`: `EC` or `RSA` + /// + /// - `privateKeyPkcs8`: the private key, PKCS#8 DER + /// + /// The server is always verified: `ConnectionRequest` refuses a client + /// certificate on an insecure request before this is called. + public void setClientCertificate(Object connection, String certificateChainPem, String keyAlgorithm, + byte[] privateKeyPkcs8) { + throw new IllegalStateException("This platform cannot present a client certificate"); + } + /// Checks if this platform supports read timeout in network connections. /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/impl/KernelHardware.java b/CodenameOne/src/com/codename1/impl/KernelHardware.java new file mode 100644 index 00000000000..2a7e28154cb --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/KernelHardware.java @@ -0,0 +1,283 @@ +/* + * 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; + +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import java.util.ArrayList; + +/// [Hardware] over the Linux kernel's own interfaces: the GPIO character +/// device, i2c-dev, spidev, termios, sysfs PWM and backlight, SocketCAN and +/// `/dev/watchdog`. The Linux port and the headless backend both extend it and +/// supply only the `raw` primitives, which they implement over one shared C +/// body (`cn1_hardware_impl.h`), so the two cannot drift apart. +@SharedWithBackend +public abstract class KernelHardware extends Hardware { + + /// Linux `ETIMEDOUT`. + private static final int ETIMEDOUT = 110; + + @Override + protected int getSupportedFeatures() { + return FEATURE_GPIO | FEATURE_I2C | FEATURE_SPI | FEATURE_SERIAL | FEATURE_PWM + | FEATURE_CAN | FEATURE_WATCHDOG | FEATURE_BACKLIGHT; + } + + private static int check(int r, String what) throws HardwareException { + if (r < 0) { + throw new HardwareException(what + " failed (errno " + (-r) + ")", -r); + } + return r; + } + + private static String[] split(String s) { + ArrayList out = new ArrayList(); + if (s != null) { + 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 + public String[] listGpioChips() { + return split(rawList(0)); + } + + @Override + public int[] listI2cBuses() { + String[] names = split(rawList(1)); + int[] out = new int[names.length]; + int count = 0; + for (String name : names) { + try { + out[count] = Integer.parseInt(name.substring(4)); + count++; + } catch (NumberFormatException ignored) { + // A non-numeric i2c- entry is not a bus node. + } + } + int[] trimmed = new int[count]; + System.arraycopy(out, 0, trimmed, 0, count); + return trimmed; + } + + @Override + public String[] listSpiDevices() { + String[] names = split(rawList(2)); + for (int i = 0; i < names.length; i++) { + names[i] = names[i].substring(6); + } + return names; + } + + @Override + public String[] listSerialPorts() { + String[] names = split(rawList(3)); + for (int i = 0; i < names.length; i++) { + names[i] = "/dev/" + names[i]; + } + return names; + } + + @Override + public String[] listCanInterfaces() { + return split(rawList(4)); + } + + @Override + public int getBacklightBrightness() { + int r = rawBacklight(0, 0); + return r < 0 ? -1 : r; + } + + @Override + public int getMaxBacklightBrightness() { + int r = rawBacklight(1, 0); + return r < 0 ? -1 : r; + } + + @Override + public void setBacklightBrightness(int value) throws HardwareException { + check(rawBacklight(2, value), "Backlight"); + } + + @Override + protected int gpioOpen(String chip, int line, boolean output, boolean initialHigh, int bias, int edges) throws HardwareException { + return check(rawGpioOpen(chip, line, output, initialHigh, bias, edges), "GPIO " + chip + "/" + line); + } + + @Override + protected boolean gpioRead(int handle) throws HardwareException { + return check(rawGpioRead(handle), "GPIO read") != 0; + } + + @Override + protected void gpioWrite(int handle, boolean high) throws HardwareException { + check(rawGpioWrite(handle, high), "GPIO write"); + } + + @Override + protected int gpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) throws HardwareException { + int r = rawGpioWaitEdge(handle, timeoutMs, timestampOut); + if (r == -ETIMEDOUT) { + return EDGE_TIMEOUT; + } + return check(r, "GPIO edge wait") == 1 ? EDGE_RESULT_RISING : EDGE_RESULT_FALLING; + } + + @Override + protected int i2cOpen(int bus, int address) throws HardwareException { + return check(rawI2cOpen(bus, address), "I2C bus " + bus); + } + + @Override + protected void i2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) throws HardwareException { + check(rawI2cTransfer(handle, write, writeLen, read, readLen), "I2C transfer"); + } + + @Override + protected int spiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + return check(rawSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord), "SPI " + bus + "." + chipSelect); + } + + @Override + protected void spiTransfer(int handle, byte[] tx, byte[] rx, int len) throws HardwareException { + check(rawSpiTransfer(handle, tx, rx, len), "SPI transfer"); + } + + @Override + protected int serialOpen(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + return check(rawSerialOpen(device, baud, dataBits, parity, stopBits), "Serial " + device); + } + + @Override + protected int serialRead(int handle, byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + return check(rawSerialRead(handle, buf, off, len, timeoutMs), "Serial read"); + } + + @Override + protected void serialWrite(int handle, byte[] buf, int off, int len) throws HardwareException { + check(rawSerialWrite(handle, buf, off, len), "Serial write"); + } + + @Override + protected int pwmOpen(int chip, int channel) throws HardwareException { + return check(rawPwmOpen(chip, channel), "PWM " + chip + "/" + channel); + } + + @Override + protected void pwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + check(rawPwmConfigure(handle, periodNanos, dutyNanos, enabled), "PWM configure"); + } + + @Override + protected int canOpen(String iface) throws HardwareException { + return check(rawCanOpen(iface), "CAN " + iface); + } + + @Override + protected void canSend(int handle, int id, boolean extended, byte[] data, int len) throws HardwareException { + check(rawCanSend(handle, id, extended, data, len), "CAN send"); + } + + @Override + protected int canReceive(int handle, int[] idOut, byte[] data, int timeoutMs) throws HardwareException { + int r = rawCanReceive(handle, idOut, data, timeoutMs); + if (r == -ETIMEDOUT) { + return -1; + } + return check(r, "CAN receive"); + } + + @Override + protected int watchdogOpen(int timeoutSeconds) throws HardwareException { + return check(rawWatchdogOpen(timeoutSeconds), "Watchdog"); + } + + @Override + protected void watchdogKick(int handle) throws HardwareException { + check(rawWatchdogKick(handle), "Watchdog kick"); + } + + @Override + protected void close(int handle, boolean disarm) { + rawClose(handle, disarm); + } + + // The kernel primitives. Each answers a handle or a result >= 0, or -errno; + // the runtime implements them over the shared C in cn1_hardware_impl.h. + + /// `kind`: 0 GPIO chips, 1 I2C buses, 2 SPI devices, 3 serial ports, + /// 4 CAN interfaces. Names separated by newlines. + protected abstract String rawList(int kind); + + /// `op`: 0 read, 1 maximum, 2 write `value`. + protected abstract int rawBacklight(int op, int value); + + protected abstract int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + + protected abstract int rawGpioRead(int handle); + + protected abstract int rawGpioWrite(int handle, boolean high); + + protected abstract int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + + protected abstract int rawI2cOpen(int bus, int address); + + protected abstract int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + + protected abstract int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + + protected abstract int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + + protected abstract int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + + protected abstract int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + + protected abstract int rawSerialWrite(int handle, byte[] buf, int off, int len); + + protected abstract int rawPwmOpen(int chip, int channel); + + protected abstract int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + + protected abstract int rawCanOpen(String iface); + + protected abstract int rawCanSend(int handle, int id, boolean extended, byte[] data, int len); + + protected abstract int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + + protected abstract int rawWatchdogOpen(int timeoutSeconds); + + protected abstract int rawWatchdogKick(int handle); + + protected abstract void rawClose(int handle, boolean disarm); +} diff --git a/CodenameOne/src/com/codename1/impl/MdnsEngine.java b/CodenameOne/src/com/codename1/impl/MdnsEngine.java new file mode 100644 index 00000000000..8374ea010b5 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/MdnsEngine.java @@ -0,0 +1,1094 @@ +/* + * 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; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/// Multicast DNS service discovery (RFC 6762 and RFC 6763) written in Java over +/// a UDP socket, for platforms without a system responder to call: the Linux +/// port, embedded boards and the headless backend. Each supplies only the +/// `raw` socket primitives, so the protocol is one implementation everywhere. +/// +/// It publishes services (probing for a unique name, announcing, answering +/// queries, saying goodbye) and browses for them (querying with back-off, +/// resolving each instance's SRV, TXT and address, and reporting it lost on a +/// goodbye or when its records expire). It answers for the host's own name +/// too, so a client can reach `.local` without another responder. +/// +/// One thread, started with the first publish or browse, owns the socket and +/// every mutable structure. Callers on other threads only replace the +/// immutable arrays of what is published and browsed, so nothing is locked. +/// Listener callbacks run on that thread; the client platform hands them to +/// the EDT, the backend calls them as they are. +/// +/// Part of the framework's implementation, not intended for application use: +/// applications use `com.codename1.io.bonjour` or the backend's `Discovery`. +@SharedWithBackend +public abstract class MdnsEngine { + + /// Receives what a browse finds. Called on the engine's thread. + public interface Listener { + /// An instance of the browsed type was resolved, or its address, port + /// or TXT record changed. + void resolved(Object browse, String name, String type, String host, int port, + Map txt); + + /// An instance said goodbye or its records expired. + void lost(Object browse, String name, String type); + + /// The engine could not start or its socket failed. + void failed(Object browse, String message); + } + + static final int TYPE_A = 1; + static final int TYPE_PTR = 12; + static final int TYPE_TXT = 16; + static final int TYPE_SRV = 33; + static final int TYPE_ANY = 255; + private static final int CLASS_IN = 1; + private static final int CACHE_FLUSH = 0x8000; + private static final int TTL_HOST = 120; + private static final int TTL_OTHER = 4500; + private static final int MAX_PACKET = 9000; + private static final String SERVICES = "_services._dns-sd._udp.local"; + + private final AtomicReference services = new AtomicReference(new Service[0]); + private final AtomicReference browses = new AtomicReference(new Browse[0]); + private final AtomicReference thread = new AtomicReference(); + + // Everything below belongs to the engine thread. + private int fd = -1; + private String hostName; + private byte[][] addresses = new byte[0][]; + private final ArrayList live = new ArrayList(); + private final ArrayList watching = new ArrayList(); + private final byte[] in = new byte[MAX_PACKET]; + + /// Opens the multicast socket: UDP port 5353, joined to 224.0.0.251 on + /// every IPv4 interface. Answers a handle >= 0, or a negative error. + protected abstract int rawOpen(); + + /// Sends a packet to 224.0.0.251:5353 on every interface. Answers >= 0 or a + /// negative error. + protected abstract int rawSend(int handle, byte[] packet, int length); + + /// Waits up to timeoutMs for a packet. Answers its length, 0 on a timeout, + /// or a negative error. + protected abstract int rawReceive(int handle, byte[] buffer, int timeoutMs); + + /// The host name without a domain, then each IPv4 address of an interface + /// that is up, not loopback and multicast capable, one per line. + protected abstract String rawHostInfo(); + + /// Closes the socket. + protected abstract void rawClose(int handle); + + /// Milliseconds on a clock that does not jump; `System.currentTimeMillis` + /// by default. Tests replace it. + protected long now() { + return System.currentTimeMillis(); + } + + // ------------------------------------------------------------------ API + + /// Publishes a service; answers a handle for [#unpublish(Object)]. The + /// type is `_name._tcp` or `_name._udp`, with or without `.local.`. A name + /// another host already uses gets a numeric suffix, as Bonjour does. + public Object publish(String name, String type, int port, Map txt) { + Service s = new Service(name, normalizeType(type), port, copy(txt)); + Service[] cur; + Service[] next; + do { + cur = services.get(); + next = new Service[cur.length + 1]; + System.arraycopy(cur, 0, next, 0, cur.length); + next[cur.length] = s; + } while (!services.compareAndSet(cur, next)); + start(); + return s; + } + + /// Withdraws a published service; its goodbye is sent at once. + public void unpublish(Object handle) { + Service[] cur; + Service[] next; + do { + cur = services.get(); + int i = indexOf(cur, handle); + if (i < 0) { + return; + } + next = new Service[cur.length - 1]; + System.arraycopy(cur, 0, next, 0, i); + System.arraycopy(cur, i + 1, next, i, next.length - i); + } while (!services.compareAndSet(cur, next)); + } + + /// Starts browsing for a type; answers a handle for [#stopBrowse(Object)]. + public Object browse(String type, Listener listener) { + Browse b = new Browse(normalizeType(type), listener); + Browse[] cur; + Browse[] next; + do { + cur = browses.get(); + next = new Browse[cur.length + 1]; + System.arraycopy(cur, 0, next, 0, cur.length); + next[cur.length] = b; + } while (!browses.compareAndSet(cur, next)); + start(); + return b; + } + + /// Stops a browse; its listener is not called again. + public void stopBrowse(Object handle) { + Browse[] cur; + Browse[] next; + do { + cur = browses.get(); + int i = indexOf(cur, handle); + if (i < 0) { + return; + } + next = new Browse[cur.length - 1]; + System.arraycopy(cur, 0, next, 0, i); + System.arraycopy(cur, i + 1, next, i, next.length - i); + } while (!browses.compareAndSet(cur, next)); + } + + /// The name a published service ended up with, which differs from the one + /// asked for when another host had it. Null for an unknown handle. + public String publishedName(Object handle) { + return handle instanceof Service ? ((Service) handle).currentName.get() : null; + } + + // Arrays are built by hand throughout: toArray(T[]) creates its result + // reflectively, which ParparVM cannot do for an arbitrary component type. + private static int indexOf(Object[] cur, Object item) { + for (int i = 0; i < cur.length; i++) { + if (cur[i] == item) { // NOPMD CompareObjectsWithEquals - a handle is found by identity + return i; + } + } + return -1; + } + + private static Map copy(Map txt) { + HashMap out = new HashMap(); + if (txt != null) { + out.putAll(txt); + } + return out; + } + + /// `_http._tcp.` or `_http._tcp.local.` to `_http._tcp.local`. + static String normalizeType(String type) { + String t = type == null ? "" : type.trim(); + while (t.endsWith(".")) { + t = t.substring(0, t.length() - 1); + } + if (!t.regionMatches(true, Math.max(0, t.length() - 6), ".local", 0, 6)) { + t = t + ".local"; + } + return t; + } + + private void start() { + if (thread.get() != null) { + return; + } + Thread t = new Thread(new Runnable() { + @Override + public void run() { + loop(); + } + }, "mDNS"); + // Discovery never keeps a process alive by itself. + t.setDaemon(true); + if (thread.compareAndSet(null, t)) { + t.start(); + } + } + + // --------------------------------------------------------------- engine + + private void loop() { + fd = rawOpen(); + if (fd < 0) { + fail("mDNS socket could not be opened (error " + (-fd) + ")"); + return; + } + readHostInfo(); + long lastHostInfo = now(); + while (true) { + long t = now(); + if (t - lastHostInfo > 30000) { + // Addresses come and go (DHCP, a cable); re-read them now and then. + readHostInfo(); + lastHostInfo = t; + } + syncServices(t); + syncBrowses(t); + if (live.isEmpty() && watching.isEmpty() && services.get().length == 0 + && browses.get().length == 0) { + // Nothing left to do; a later publish or browse starts a new thread. + rawClose(fd); + fd = -1; + thread.set(null); + if (services.get().length == 0 && browses.get().length == 0) { + return; + } + start(); + return; + } + long next = t + 1000; + for (Service s : live) { + next = Math.min(next, s.due); + } + for (Browse b : watching) { + next = Math.min(next, b.nextQuery); + } + int wait = (int) Math.max(20, Math.min(1000, next - now())); + int n = rawReceive(fd, in, wait); + if (n < 0) { + fail("mDNS receive failed (error " + (-n) + ")"); + rawClose(fd); + fd = -1; + thread.set(null); + return; + } + if (n > 0) { + handlePacket(in, n); + } + } + } + + private void fail(String message) { + Browse[] b = browses.get(); + for (Browse each : b) { + each.listener.failed(each, message); + } + } + + private void readHostInfo() { + String info = rawHostInfo(); + ArrayList list = new ArrayList(); + String host = null; + if (info != null) { + int start = 0; + for (int i = 0; i <= info.length(); i++) { + if (i == info.length() || info.charAt(i) == '\n') { + String line = info.substring(start, i).trim(); + start = i + 1; + if (line.length() == 0) { + continue; + } + if (host == null) { + host = line; + } else { + byte[] a = parseIpv4(line); + if (a != null) { + list.add(a); + } + } + } + } + } + if (host == null || host.length() == 0) { + host = "codenameone"; + } + int dot = host.indexOf('.'); + if (dot > 0) { + host = host.substring(0, dot); + } + hostName = host + ".local"; + byte[][] out = new byte[list.size()][]; + for (int i = 0; i < out.length; i++) { + out[i] = list.get(i); + } + addresses = out; + } + + static byte[] parseIpv4(String s) { + byte[] out = new byte[4]; + int part = 0; + int value = -1; + for (int i = 0; i <= s.length(); i++) { + char c = i == s.length() ? '.' : s.charAt(i); + if (c == '.') { + if (value < 0 || value > 255 || part > 3) { + return null; + } + out[part++] = (byte) value; + value = -1; + } else if (c >= '0' && c <= '9') { + value = (value < 0 ? 0 : value * 10) + (c - '0'); + } else { + return null; + } + } + return part == 4 ? out : null; + } + + // ------------------------------------------------------------ publishing + + private void syncServices(long t) { + Service[] want = services.get(); + // Withdrawn services say goodbye. + for (Iterator it = live.iterator(); it.hasNext();) { + Service s = it.next(); + if (!contains(want, s)) { + it.remove(); + if (s.state >= Service.ANNOUNCING) { + Packet p = new Packet(true); + addServiceRecords(p, s, 0); + send(p); + } + } + } + for (int i = 0; i < want.length; i++) { + if (!live.contains(want[i])) { + want[i].due = t; + live.add(want[i]); + } + } + for (Service s : live) { + if (s.due > t) { + continue; + } + if (s.state == Service.PROBING) { + if (s.step < 3) { + // A probe asks who owns the name and proposes our records. + Packet p = new Packet(false); + p.question(s.instance(), TYPE_ANY, true); + p.section = AUTHORITY; + p.srv(s.instance(), TTL_HOST, s.port, hostName, false); + send(p); + s.step++; + s.due = t + 250; + } else { + s.state = Service.ANNOUNCING; + s.step = 0; + s.due = t; + } + } + if (s.state == Service.ANNOUNCING && s.due <= t) { + Packet p = new Packet(true); + addServiceRecords(p, s, -1); + send(p); + s.step++; + if (s.step >= 2) { + s.state = Service.LIVE; + s.due = Long.MAX_VALUE; + } else { + s.due = t + 1000; + } + } + } + } + + private static boolean contains(Object[] a, Object o) { + for (Object each : a) { + if (each == o) { // NOPMD CompareObjectsWithEquals - membership by identity + return true; + } + } + return false; + } + + /// The PTR, SRV and TXT records of a service and the host's addresses; ttl + /// -1 means each record's usual TTL, 0 a goodbye. + private void addServiceRecords(Packet p, Service s, int ttl) { + p.ptr(s.type, ttl < 0 ? TTL_OTHER : ttl, s.instance()); + p.ptr(SERVICES, ttl < 0 ? TTL_OTHER : ttl, s.type); + p.srv(s.instance(), ttl < 0 ? TTL_HOST : ttl, s.port, hostName, true); + p.txt(s.instance(), ttl < 0 ? TTL_OTHER : ttl, s.txt); + if (ttl != 0) { + addAddresses(p); + } + } + + private void addAddresses(Packet p) { + for (byte[] address : addresses) { + p.a(hostName, TTL_HOST, address); + } + } + + private void answer(String name, int type) { + Packet p = new Packet(true); + boolean any = type == TYPE_ANY; + for (Service s : live) { + if (s.state == Service.PROBING) { + continue; + } + if ((type == TYPE_PTR || any) && name.equalsIgnoreCase(s.type)) { + p.section = ANSWER; + p.ptr(s.type, TTL_OTHER, s.instance()); + // The rest of what a browser needs travels as additionals. + p.section = ADDITIONAL; + p.srv(s.instance(), TTL_HOST, s.port, hostName, true); + p.txt(s.instance(), TTL_OTHER, s.txt); + addAddresses(p); + } else if ((type == TYPE_PTR || any) && SERVICES.equalsIgnoreCase(name)) { + p.section = ANSWER; + p.ptr(SERVICES, TTL_OTHER, s.type); + } else if (name.equalsIgnoreCase(s.instance())) { + p.section = ANSWER; + if (type == TYPE_SRV || any) { + p.srv(s.instance(), TTL_HOST, s.port, hostName, true); + } + if (type == TYPE_TXT || any) { + p.txt(s.instance(), TTL_OTHER, s.txt); + } + p.section = ADDITIONAL; + addAddresses(p); + } + } + if ((type == TYPE_A || any) && name.equalsIgnoreCase(hostName) && !live.isEmpty()) { + p.section = ANSWER; + addAddresses(p); + } + if (p.answers() > 0) { + send(p); + } + } + + private void send(Packet p) { + if (fd >= 0) { + byte[] bytes = p.toBytes(); + rawSend(fd, bytes, bytes.length); + } + } + + // ------------------------------------------------------------- browsing + + private void syncBrowses(long t) { + Browse[] want = browses.get(); + for (Iterator it = watching.iterator(); it.hasNext();) { + if (!contains(want, it.next())) { + it.remove(); + } + } + for (int i = 0; i < want.length; i++) { + if (!watching.contains(want[i])) { + want[i].nextQuery = t; + watching.add(want[i]); + } + } + for (Browse b : watching) { + // Expired instances are lost; incomplete ones are asked about again. + for (Iterator it = b.instances.values().iterator(); it.hasNext();) { + Instance inst = it.next(); + if (inst.expires <= t) { + it.remove(); + if (inst.reported) { + b.listener.lost(b, inst.label, displayType(b.type)); + } + } else if (!inst.complete() && inst.nextAsk <= t) { + Packet p = new Packet(false); + if (inst.target == null) { + p.question(inst.fullName, TYPE_SRV, false); + } + if (inst.txt == null) { + p.question(inst.fullName, TYPE_TXT, false); + } + if (inst.target != null && inst.host == null) { + p.question(inst.target, TYPE_A, false); + } + send(p); + inst.nextAsk = t + 1000; + } + } + if (b.nextQuery <= t) { + Packet p = new Packet(false); + p.question(b.type, TYPE_PTR, false); + send(p); + b.interval = b.interval == 0 ? 1000 : Math.min(b.interval * 2, 60000); + b.nextQuery = t + b.interval; + } + } + } + + private static String displayType(String type) { + // _http._tcp.local to the _http._tcp. form BonjourService reports. + return type.substring(0, type.length() - 5); + } + + // -------------------------------------------------------------- packets + + private void handlePacket(byte[] data, int length) { + Reader r = new Reader(data, length); + ArrayList records = new ArrayList(); + try { + r.pos = 2; + int flags = r.u16(); + int qd = r.u16(); + int an = r.u16(); + int ns = r.u16(); + int ar = r.u16(); + boolean response = (flags & 0x8000) != 0; + for (int i = 0; i < qd; i++) { + String name = r.name(); + int type = r.u16(); + r.u16(); + if (!response) { + answer(name, type); + } + } + if (!response) { + // A probe's authority section is the other host's claim. + for (int i = 0; i < ns; i++) { + Record rec = r.record(); + if (rec != null) { + checkConflict(rec, true); + } + } + return; + } + int total = an + ns + ar; + for (int i = 0; i < total; i++) { + Record rec = r.record(); + if (rec != null) { + records.add(rec); + } + } + } catch (RuntimeException malformed) { + // A truncated or malformed packet from the network is ignored. + return; + } + // Acted on outside the try, so nothing here relies on the catch. + for (Record rec : records) { + checkConflict(rec, false); + } + absorb(records); + } + + /// Another host claiming the name one of our services is probing for, or + /// already uses, renames ours: "Name (2)", then "(3)" and so on. + private void checkConflict(Record rec, boolean probe) { + if (rec.type != TYPE_SRV && rec.type != TYPE_TXT) { + return; + } + for (Service s : live) { + if (!rec.name.equalsIgnoreCase(s.instance())) { + continue; + } + if (rec.type == TYPE_SRV && rec.port == s.port && hostName.equalsIgnoreCase(rec.target)) { + // Our own packet, looped back. + continue; + } + if (rec.type == TYPE_TXT) { + continue; + } + // Two hosts probing for one name at once: the lexicographically later + // claim keeps it (RFC 6762 8.2, compared here on port then target), + // so exactly one of them renames rather than both, forever. + boolean lose = !probe || (s.state == Service.PROBING && (rec.port > s.port + || (rec.port == s.port && asciiLower(rec.target).compareTo(asciiLower(hostName)) > 0))); + if (lose) { + s.suffix++; + s.currentName.set(s.baseName + " (" + (s.suffix + 1) + ")"); + s.state = Service.PROBING; + s.step = 0; + s.due = now() + 250; + } + } + } + + private void absorb(ArrayList records) { + long t = now(); + for (Browse b : watching) { + for (Record rec : records) { + if (rec.type == TYPE_PTR && rec.name.equalsIgnoreCase(b.type)) { + String key = asciiLower(rec.target); + Instance inst = b.instances.get(key); + if (rec.ttl == 0) { + if (inst != null) { + // A goodbye: gone in a second unless re-announced (RFC 6762 10.1). + inst.expires = Math.min(inst.expires, t + 1000); + } + continue; + } + if (inst == null) { + inst = new Instance(rec.target, label(rec.target, b.type)); + b.instances.put(key, inst); + } + inst.expires = t + rec.ttl * 1000L; + } + } + for (Instance inst : b.instances.values()) { + boolean changed = false; + for (Record rec : records) { + if (rec.type == TYPE_SRV && rec.name.equalsIgnoreCase(inst.fullName) && rec.ttl > 0) { + if (inst.port != rec.port || !rec.target.equalsIgnoreCase(inst.target)) { + inst.port = rec.port; + inst.target = rec.target; + inst.host = null; + changed = true; + } + } else if (rec.type == TYPE_TXT && rec.name.equalsIgnoreCase(inst.fullName) && rec.ttl > 0) { + if (inst.txt == null || !inst.txt.equals(rec.txt)) { + inst.txt = rec.txt; + changed = true; + } + } + } + if (inst.target != null) { + for (Record rec : records) { + if (rec.type == TYPE_A && rec.name.equalsIgnoreCase(inst.target) && rec.ttl > 0) { + if (!rec.address.equals(inst.host)) { + inst.host = rec.address; + changed = true; + } + break; + } + } + } + if (inst.complete() && (changed || !inst.reported)) { + inst.reported = true; + b.listener.resolved(b, inst.label, displayType(b.type), inst.host, inst.port, + new HashMap(inst.txt)); + } + } + } + } + + /// The instance label of `My Device._http._tcp.local`. + private static String label(String fullName, String type) { + int end = fullName.length() - type.length() - 1; + return end > 0 ? fullName.substring(0, end) : fullName; + } + + /// ASCII-only lower case: `toLowerCase` folds by locale, and a DNS name is + /// compared without regard to ASCII case only (RFC 4343). + static String asciiLower(String s) { + char[] c = s.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (c[i] >= 'A' && c[i] <= 'Z') { + c[i] = (char) (c[i] + 32); + } + } + return new String(c); + } + + // ---------------------------------------------------------------- model + + static final class Service { + static final int PROBING = 0; + static final int ANNOUNCING = 1; + static final int LIVE = 2; + + final String baseName; + final AtomicReference currentName; + final String type; + final int port; + final Map txt; + int state; + int step; + int suffix; + long due; + + Service(String name, String type, int port, Map txt) { + String n = name == null || name.length() == 0 ? "Codename One" : name; + // A label is at most 63 bytes; leave room for a " (n)" suffix. + while (utf8(n).length > 56) { + n = n.substring(0, n.length() - 1); + } + this.baseName = n; + this.currentName = new AtomicReference(n); + this.type = type; + this.port = port; + this.txt = txt; + } + + String instance() { + return currentName.get() + "." + type; + } + } + + static final class Browse { + final String type; + final Listener listener; + final HashMap instances = new HashMap(); + long nextQuery; + long interval; + + Browse(String type, Listener listener) { + this.type = type; + this.listener = listener; + } + } + + static final class Instance { + final String fullName; + final String label; + String target; + int port; + String host; + Map txt; + long expires; + long nextAsk; + boolean reported; + + Instance(String fullName, String label) { + this.fullName = fullName; + this.label = label; + } + + boolean complete() { + return target != null && host != null && txt != null; + } + } + + static final class Record { + String name; + int type; + long ttl; + // Empty rather than null for a record of another type, so a match on + // the type is all a reader checks. + String target = ""; + int port; + String address = ""; + Map txt; + } + + // ------------------------------------------------------------- encoding + + static byte[] utf8(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // Every runtime has UTF-8; this cannot happen. + throw new IllegalStateException("UTF-8", e); + } + } + + static String utf8(byte[] b, int off, int len) { + try { + return new String(b, off, len, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8", e); + } + } + + /// Splits a name into labels. The first label of a service instance may + /// contain dots, so instance names are split after it by the caller. + private static void writeName(Writer w, String name) { + String rest = name; + // An instance name is "