Merge branch 'master' into NextHopRouter

This commit is contained in:
Tom Fifield 2025-02-02 10:18:32 +08:00 committed by GitHub
commit b91b66bc7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
96 changed files with 1752 additions and 542 deletions

View File

@ -0,0 +1,52 @@
# This container is used to build Meshtastic with the libraries required by the fuzzer.
# ClusterFuzzLite starts the container, runs the build.sh script, and then exits.
# As this is not a long running service, health-checks are not required. ClusterFuzzLite
# also only works if the user remains unchanged from the base image (it expects to run
# as root).
# trunk-ignore-all(trivy/DS026): No healthcheck is needed for this builder container
# trunk-ignore-all(checkov/CKV_DOCKER_2): No healthcheck is needed for this builder container
# trunk-ignore-all(checkov/CKV_DOCKER_3): We must run as root for this container
# trunk-ignore-all(trivy/DS002): We must run as root for this container
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container
FROM gcr.io/oss-fuzz-base/base-builder:v1
ENV PIP_ROOT_USER_ACTION=ignore
# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -U \
platformio==6.1.16 \
grpcio-tools==1.68.1 \
meshtastic==2.5.9
# Ugly hack to avoid clang detecting a conflict between the math "log" function and the "log" function in framework-portduino/cores/portduino/logging.h
RUN sed -i -e 's/__MATHCALL_VEC (log,, (_Mdouble_ __x));//' /usr/include/x86_64-linux-gnu/bits/mathcalls.h
# A few dependencies are too old on the base-builder image. More recent versions are built from source.
WORKDIR $SRC
RUN git config --global advice.detachedHead false && \
git clone --depth 1 --branch 0.8.0 https://github.com/jbeder/yaml-cpp.git && \
git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania.git && \
git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder.git && \
git clone --depth 1 --branch v2.7.15 https://github.com/babelouest/ulfius.git
COPY ./.clusterfuzzlite/build.sh $SRC/
WORKDIR $SRC/firmware
COPY . $SRC/firmware/
# https://docs.platformio.org/en/latest/envvars.html
ENV PLATFORMIO_CORE_DIR=$SRC/pio/core \
PLATFORMIO_LIBDEPS_DIR=$SRC/pio/libdeps \
PLATFORMIO_PACKAGES_DIR=$SRC/pio/packages \
PLATFORMIO_SETTING_ENABLE_CACHE=No \
PIO_ENV=buildroot
RUN platformio pkg install --environment $PIO_ENV

View File

@ -0,0 +1,59 @@
# ClusterFuzzLite for Meshtastic
This directory contains the fuzzer implementation for Meshtastic using the ClusterFuzzLite framework.
See the [ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/) for more details.
## Running locally
ClusterFuzzLite uses the OSS-Fuzz toolchain. To build the fuzzer manually, first grab a copy of OSS-Fuzz.
```shell
git clone https://github.com/google/oss-fuzz.git
cd oss-fuzz
```
To build the fuzzer, run:
```shell
python3 infra/helper.py build_image --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY
python3 infra/helper.py build_fuzzers --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY --sanitizer address
```
To run the fuzzer, run:
```shell
python3 infra/helper.py run_fuzzer --external --corpus-dir=<path-to-temp-corpus-dir> $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY router_fuzzer
```
More background on these commands can be found in the
[ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/build-integration/#testing-locally).
## router_fuzzer.cpp
This fuzzer submits MeshPacket protos to the `Router::enqueueReceivedMessage` method. It takes the binary
data from the fuzzer and decodes that data to a MeshPacket using nanopb. A few fields in
the MeshPacket are modified by the fuzzer.
- If the `to` field is 0, it will be replaced with the NodeID of the running node.
- If the `from` field is 0, it will be replaced with the NodeID of the running node.
- If the `id` field is 0, it will be replaced with an incrementing counter value.
- If the `pki_encrypted` field is true, the `public_key` field will be populated with the first admin key.
The `router_fuzzer_seed_corpus.py` file contains a list of MeshPackets. It is run from inside build.sh and
writes the binary MeshPacket protos to files. These files are use used by the fuzzer as its initial seed data,
helping the fuzzer to start off with a few known inputs.
### Interpreting a fuzzer crash
If the fuzzer crashes, it'll write the input bytes used for the test case to a file and notify about the
location of that file. The contents of the file are a binary serialized MeshPacket protobuf. The following
snippet of Python code can be used to parse the file into a human readable form.
```python
from meshtastic.protobuf import mesh_pb2
mesh_pb2.MeshPacket.FromString(open("crash-XXXX-file", "rb").read())
```
Consider adding any such crash results to the `router_fuzzer_seed_corpus.py` file to ensure there a isn't
a future regression for that crash test case.

71
.clusterfuzzlite/build.sh Normal file
View File

@ -0,0 +1,71 @@
#!/bin/bash -eu
# Build Meshtastic and a few needed dependencies using clang++
# and the OSS-Fuzz required build flags.
env
cd "$SRC"
NPROC=$(nproc || echo 1)
LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp/$SANITIZER" \
-DBUILD_SHARED_LIBS=OFF
cmake --build "$WORK/yaml-cpp/$SANITIZER" -j "$NPROC"
cmake --install "$WORK/yaml-cpp/$SANITIZER" --prefix /usr
cmake -S "$SRC/orcania" -B "$WORK/orcania/$SANITIZER" \
-DBUILD_STATIC=ON
cmake --build "$WORK/orcania/$SANITIZER" -j "$NPROC"
cmake --install "$WORK/orcania/$SANITIZER" --prefix /usr
cmake -S "$SRC/yder" -B "$WORK/yder/$SANITIZER" \
-DBUILD_STATIC=ON -DWITH_JOURNALD=OFF
cmake --build "$WORK/yder/$SANITIZER" -j "$NPROC"
cmake --install "$WORK/yder/$SANITIZER" --prefix /usr
cmake -S "$SRC/ulfius" -B "$WORK/ulfius/$SANITIZER" \
-DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF
cmake --build "$WORK/ulfius/$SANITIZER" -j "$NPROC"
cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr
cd "$SRC/firmware"
PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
export PLATFORMIO_EXTRA_SCRIPTS
export STATIC_LIBS
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
export TARGET_CC=$CC
export TARGET_CXX=$CXX
export TARGET_LD=$CXX
export TARGET_AR=llvm-ar
export TARGET_AS=llvm-as
export TARGET_OBJCOPY=llvm-objcopy
export TARGET_RANLIB=llvm-ranlib
mkdir -p "$OUT/lib"
cp .clusterfuzzlite/*_fuzzer.options "$OUT/"
for f in .clusterfuzzlite/*_fuzzer.cpp; do
fuzzer=$(basename "$f" .cpp)
cp -f "$f" src/fuzzer.cpp
pio run -vvv --environment "$PIO_ENV"
program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program"
cp "$program" "$OUT/$fuzzer"
# Copy shared libraries used by the fuzzer.
read -d '' -ra shared_libs < <(ldd "$program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') || true
cp -f "${shared_libs[@]}" "$OUT/lib/"
# Build the initial fuzzer seed corpus.
corpus_name="${fuzzer}_seed_corpus"
corpus_generator="$PWD/.clusterfuzzlite/${corpus_name}.py"
if [[ -f $corpus_generator ]]; then
mkdir "$corpus_name"
pushd "$corpus_name"
python3 "$corpus_generator"
popd
zip -D "$OUT/${corpus_name}.zip" "$corpus_name"/*
fi
done

View File

@ -0,0 +1,35 @@
"""PlatformIO build script (post: runs after other Meshtastic scripts)."""
import os
import shlex
from SCons.Script import DefaultEnvironment
env = DefaultEnvironment()
# Remove any static libraries from the LIBS environment. Static libraries are
# handled in platformio-clusterfuzzlite-pre.py.
static_libs = set(lib[2:] for lib in shlex.split(os.getenv("STATIC_LIBS")))
env.Replace(
LIBS=[
lib for lib in env["LIBS"] if not (isinstance(lib, str) and lib in static_libs)
],
)
# FrameworkArduino/portduino/main.cpp contains the "main" function the binary.
# The fuzzing framework also provides a "main" function and needs to be run
# before Meshtastic is started. We rename the "main" function for Meshtastic to
# "portduino_main" here so that it can be called inside the fuzzer.
env.AddPostAction(
"$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o",
env.VerboseAction(
" ".join(
[
"$OBJCOPY",
"--redefine-sym=main=portduino_main",
"$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o",
]
),
"Renaming main symbol to portduino_main",
),
)

View File

@ -0,0 +1,52 @@
"""PlatformIO build script (pre: runs before other Meshtastic scripts).
ClusterFuzzLite executes in a different container from the build. During the build,
attempt to link statically to as many dependencies as possible. For dependencies that
do not have static libraries, the shared library files are copied to the output
directory by the build.sh script.
"""
import glob
import os
import shlex
from SCons.Script import DefaultEnvironment, Literal
env = DefaultEnvironment()
cxxflags = shlex.split(os.getenv("CXXFLAGS"))
sanitizer_flags = shlex.split(os.getenv("SANITIZER_FLAGS"))
lib_fuzzing_engine = shlex.split(os.getenv("LIB_FUZZING_ENGINE"))
statics = glob.glob("/usr/lib/lib*.a") + glob.glob("/usr/lib/*/lib*.a")
no_static = set(("-ldl",))
def replaceStatic(lib):
"""Replace -l<libname> with the static .a file for the library."""
if not lib.startswith("-l") or lib in no_static:
return lib
static_name = f"/lib{lib[2:]}.a"
static = [s for s in statics if s.endswith(static_name)]
if len(static) == 1:
return static[0]
return lib
# Setup the environment for building with Clang and the OSS-Fuzz required build flags.
env.Append(
CFLAGS=os.getenv("CFLAGS"),
CXXFLAGS=cxxflags,
LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"],
LINKFLAGS=cxxflags
+ sanitizer_flags
+ lib_fuzzing_engine
+ ["-stdlib=libc++", "-std=c++17"],
_LIBFLAGS=[replaceStatic(s) for s in shlex.split(os.getenv("STATIC_LIBS"))]
+ [
"/usr/lib/x86_64-linux-gnu/libunistring.a", # Needs to be at the end.
# Find the shared libraries in a subdirectory named lib
# within the same directory as the binary.
Literal("-Wl,-rpath,$ORIGIN/lib"),
"-Wl,-z,origin",
],
)

View File

@ -0,0 +1 @@
language: c++

View File

@ -0,0 +1,206 @@
// Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage.
#include <condition_variable>
#include <cstdlib>
#include <mutex>
#include <pb_decode.h>
#include <stdexcept>
#include <string>
#include <thread>
#include "PortduinoGPIO.h"
#include "PortduinoGlue.h"
#include "PowerFSM.h"
#include "mesh/MeshTypes.h"
#include "mesh/NodeDB.h"
#include "mesh/Router.h"
#include "mesh/TypeConversions.h"
#include "mesh/mesh-pb-constants.h"
namespace
{
constexpr uint32_t nodeId = 0x12345678;
// Set to true when lateInitVariant finishes. Used to ensure lateInitVariant was called during startup.
bool hasBeenConfigured = false;
// These are used to block the Arduino loop() function until a fuzzer input is ready. This is
// an optimization that prevents a sleep from happening before the loop is run. The Arduino loop
// function calls loopCanSleep() before sleeping. loopCanSleep is implemented here in the fuzzer
// and blocks until runLoopOnce() is called to signal for the loop to run.
bool fuzzerRunning = false; // Set to true once LLVMFuzzerTestOneInput has started running.
bool loopCanRun = true; // The main Arduino loop() can run when this is true.
bool loopIsWaiting = false; // The main Arduino loop() is waiting to be signaled to run.
bool loopShouldExit = false; // Indicates that the main Arduino thread should exit by throwing ShouldExitException.
std::mutex loopLock;
std::condition_variable loopCV;
std::thread meshtasticThread;
// This exception is thrown when the portuino main thread should exit.
class ShouldExitException : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};
// Start the loop for one test case and wait till the loop has completed. This ensures fuzz
// test cases do not overlap with one another. This helps the fuzzer attribute a crash to the
// single, currently running, test case.
void runLoopOnce()
{
realHardware = true; // Avoids delay(100) within portduino/main.cpp
std::unique_lock<std::mutex> lck(loopLock);
fuzzerRunning = true;
loopCanRun = true;
loopCV.notify_one();
loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; });
}
} // namespace
// Called in the main Arduino loop function to determine if the loop can delay/sleep before running again.
// We use this as a way to block the loop from sleeping and to start the loop function immediately when a
// fuzzer input is ready.
bool loopCanSleep()
{
std::unique_lock<std::mutex> lck(loopLock);
loopIsWaiting = true;
loopCV.notify_one();
loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; });
loopIsWaiting = false;
if (loopShouldExit)
throw ShouldExitException("exit");
if (!fuzzerRunning)
return true; // The loop can sleep before the fuzzer starts.
loopCanRun = false; // Only run the loop once before waiting again.
return false;
}
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
void lateInitVariant()
{
settingsMap[logoutputlevel] = level_error;
channelFile.channels[0] = meshtastic_Channel{
.has_settings = true,
.settings =
meshtastic_ChannelSettings{
.psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}},
.name = "LongFast",
.uplink_enabled = true,
.has_module_settings = true,
.module_settings = {.position_precision = 16},
},
.role = meshtastic_Channel_Role_PRIMARY,
};
config.security.admin_key[0] = {
.size = 32,
.bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a,
0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c},
};
config.security.admin_key_count = 1;
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
moduleConfig.has_mqtt = true;
moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{
.enabled = true,
.proxy_to_client_enabled = true,
};
moduleConfig.has_store_forward = true;
moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{
.enabled = true,
.history_return_max = 4,
.history_return_window = 600,
.is_server = true,
};
meshtastic_Position fixedGPS = meshtastic_Position{
.has_latitude_i = true,
.latitude_i = static_cast<uint32_t>(1 * 1e7),
.has_longitude_i = true,
.longitude_i = static_cast<uint32_t>(3 * 1e7),
.has_altitude = true,
.altitude = 64,
.location_source = meshtastic_Position_LocSource_LOC_MANUAL,
};
nodeDB->setLocalPosition(fixedGPS);
config.has_position = true;
config.position.fixed_position = true;
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum());
info->has_position = true;
info->position = TypeConversions::ConvertToPositionLite(fixedGPS);
hasBeenConfigured = true;
}
extern "C" {
int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary.
// Start Meshtastic in a thread and wait till it has reached the ON state.
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
settingsMap[maxtophone] = 5;
meshtasticThread = std::thread([program = *argv[0]]() {
char nodeIdStr[12];
strcpy(nodeIdStr, std::to_string(nodeId).c_str());
int argc = 7;
char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr};
try {
portduino_main(argc, argv);
} catch (const ShouldExitException &) {
}
});
std::atexit([] {
{
const std::lock_guard<std::mutex> lck(loopLock);
loopShouldExit = true;
loopCV.notify_one();
}
meshtasticThread.join();
});
// Wait for startup.
for (int i = 1; i < 20; ++i) {
if (powerFSM.getState() == &stateON) {
assert(hasBeenConfigured);
assert(router);
assert(nodeDB);
return 0;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 1;
}
// This is the main entrypoint for the fuzzer (the fuzz target). The fuzzer will provide an array of bytes to be
// interpreted by this method. To keep things simple, the bytes are interpreted as a binary serialized MeshPacket
// proto. Any crashes discovered by the fuzzer will be written to a file. Unserialize that file to print the MeshPacket
// that caused the failure.
//
// This guide provides best practices for writing a fuzzer target.
// https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default;
pb_istream_t stream = pb_istream_from_buffer(data, length);
// Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa.
if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed ||
p.public_key.size || p.next_hop || p.relay_node || p.tx_after)
return -1; // Reject: The input will not be added to the corpus.
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
meshtastic_Data d;
stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size);
if (!pb_decode(&stream, &meshtastic_Data_msg, &d))
return -1; // Reject: The input will not be added to the corpus.
}
// Provide default values for a few fields so the fuzzer doesn't need to guess them.
if (p.from == 0)
p.from = nodeDB->getNodeNum();
if (p.to == 0)
p.to = nodeDB->getNodeNum();
static uint32_t packetId = 0;
if (p.id == 0)
p.id == ++packetId;
if (p.pki_encrypted && config.security.admin_key_count)
memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key));
router->enqueueReceivedMessage(packetPool.allocCopy(p));
runLoopOnce();
return 0; // Accept: The input may be added to the corpus.
}
}

View File

@ -0,0 +1,2 @@
[libfuzzer]
max_len=256

View File

@ -0,0 +1,168 @@
"""Generate an initial set of MeshPackets.
The fuzzer uses these MeshPackets as an initial seed of test candidates.
It's also good to add any previously discovered crash test cases to this list
to avoid future regressions.
If left unset, the following values will be automatically set by the fuzzer.
- to: automatically set to the running node's NodeID
- from: automatically set to the running node's NodeID
- id: automatically set to the value of an incrementing counter
Additionally, if `pki_encrypted` is populated in the packet, the first admin key
will be copied into the `public_key` field.
"""
import base64
from meshtastic import BROADCAST_NUM
from meshtastic.protobuf import (
admin_pb2,
atak_pb2,
mesh_pb2,
portnums_pb2,
telemetry_pb2,
)
def From(node: int = 9):
"""Return a dict suitable for **kwargs for populating the 'from' field.
'from' is a reserved keyword in Python. It can't be used directly as an
argument to the MeshPacket constructor. Rather **From() can be used as
the final argument to provide the from node as a **kwarg.
Defaults to 9 if no value is provided.
"""
return {"from": node}
packets = (
(
"position",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.POSITION_APP,
payload=mesh_pb2.Position(
latitude_i=int(1 * 1e7),
longitude_i=int(2 * 1e7),
altitude=5,
precision_bits=32,
).SerializeToString(),
),
to=BROADCAST_NUM,
**From(),
),
),
(
"telemetry",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.TELEMETRY_APP,
payload=telemetry_pb2.Telemetry(
time=1736192207,
device_metrics=telemetry_pb2.DeviceMetrics(
battery_level=101,
channel_utilization=8,
air_util_tx=2,
uptime_seconds=42,
),
).SerializeToString(),
),
to=BROADCAST_NUM,
**From(),
),
),
(
"text",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.TEXT_MESSAGE_APP,
payload=b"Hello world",
),
to=BROADCAST_NUM,
**From(),
),
),
(
"user",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.NODEINFO_APP,
payload=mesh_pb2.User(
id="!00000009",
long_name="Node 9",
short_name="N9",
macaddr=b"\x00\x00\x00\x00\x00\x09",
hw_model=mesh_pb2.HardwareModel.RAK4631,
public_key=base64.b64decode(
"L0ih/6F41itofdE8mYyHk1SdfOJ/QRM1KQ+pO4vEEjQ="
),
).SerializeToString(),
),
**From(),
),
),
(
"traceroute",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.TRACEROUTE_APP,
payload=mesh_pb2.RouteDiscovery(
route=[10],
).SerializeToString(),
),
**From(),
),
),
(
"routing",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.ROUTING_APP,
payload=mesh_pb2.Routing(
error_reason=mesh_pb2.Routing.NO_RESPONSE,
).SerializeToString(),
),
**From(),
),
),
(
"admin",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.ADMIN_APP,
payload=admin_pb2.AdminMessage(
get_owner_request=True,
).SerializeToString(),
),
pki_encrypted=True,
**From(),
),
),
(
"atak",
mesh_pb2.MeshPacket(
decoded=mesh_pb2.Data(
portnum=portnums_pb2.PortNum.ATAK_PLUGIN,
payload=atak_pb2.TAKPacket(
is_compressed=True,
# Note, the strings are not valid for a compressed message, but will
# give the fuzzer a starting point.
contact=atak_pb2.Contact(
callsign="callsign", device_callsign="device_callsign"
),
chat=atak_pb2.GeoChat(
message="message", to="to", to_callsign="to_callsign"
),
).SerializeToString(),
),
**From(),
),
),
)
for name, packet in packets:
with open(f"{name}.MeshPacket", "wb") as f:
f.write(packet.SerializeToString())

1
.dockerignore Symbolic link
View File

@ -0,0 +1 @@
.gitignore

View File

@ -1,51 +0,0 @@
name: Build Docker
on: workflow_call
permissions:
contents: write
packages: write
jobs:
build-native:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Docker login
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Docker setup
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
uses: docker/setup-buildx-action@v3
- name: Docker build and push tagged versions
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: meshtastic/meshtasticd:${{ steps.version.outputs.long }}
- name: Docker build and push
if: ${{ github.ref == 'refs/heads/master' && github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: meshtastic/meshtasticd:latest

View File

@ -20,6 +20,12 @@ permissions:
packages: write
jobs:
docker-multiarch:
uses: ./.github/workflows/docker_manifest.yml
with:
release_channel: daily
secrets: inherit
package-ppa:
strategy:
fail-fast: false
@ -34,7 +40,7 @@ jobs:
package-obs:
uses: ./.github/workflows/package_obs.yml
with:
obs_project: home:meshtastic:daily
obs_project: network:Meshtastic:daily
series: unstable
secrets: inherit

92
.github/workflows/docker_build.yml vendored Normal file
View File

@ -0,0 +1,92 @@
name: Build Docker
# Build Docker image, push untagged (digest-only)
on:
workflow_call:
secrets:
DOCKER_FIRMWARE_TOKEN:
required: false # Only required for push
inputs:
distro:
description: Distro to target
required: true
type: string
# choices: [debian, alpine]
platform:
description: Platform to target
required: true
type: string
runs-on:
description: Runner to use
required: true
type: string
push:
description: Push images to registry
required: false
type: boolean
default: false
outputs:
digest:
description: Digest of built image
value: ${{ jobs.docker-build.outputs.digest }}
permissions:
contents: write
packages: write
jobs:
docker-build:
outputs:
digest: ${{ steps.docker_variant.outputs.digest }}
runs-on: ${{ inputs.runs-on }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Docker login
if: ${{ inputs.push }}
uses: docker/login-action@v3
with:
username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Docker setup
uses: docker/setup-buildx-action@v3
- name: Sanitize platform string
id: sanitize_platform
# Replace slashes with underscores
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
- name: Docker tag
id: meta
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
GHA-${{ steps.version.outputs.long }}-${{ inputs.distro }}-${{ steps.sanitize_platform.outputs.cleaned_platform }}
flavor: latest=false
- name: Docker build and push
uses: docker/build-push-action@v6
id: docker_variant
with:
context: .
file: |
${{ contains(inputs.distro, 'debian') && './Dockerfile' || contains(inputs.distro, 'alpine') && './alpine.Dockerfile' }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }} # Tag is only meant to be consumed by the "manifest" job
platforms: ${{ inputs.platform }}

186
.github/workflows/docker_manifest.yml vendored Normal file
View File

@ -0,0 +1,186 @@
name: Build Docker Multi-Arch Manifest
on:
workflow_call:
secrets:
DOCKER_FIRMWARE_TOKEN:
required: true
inputs:
release_channel:
description: Release channel to target
required: true
type: string
permissions:
contents: write
packages: write
jobs:
docker-debian-amd64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: true
secrets: inherit
docker-debian-arm64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-debian-armv7:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-alpine-amd64:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: true
secrets: inherit
docker-alpine-arm64:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-alpine-armv7:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: true
secrets: inherit
docker-manifest:
needs:
# Debian
- docker-debian-amd64
- docker-debian-arm64
- docker-debian-armv7
# Alpine
- docker-alpine-amd64
- docker-alpine-arm64
- docker-alpine-armv7
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "short=$(./bin/buildinfo.py short)" >> $GITHUB_OUTPUT
id: version
- name: Enumerate tags
shell: python
run: |
import os
short = "${{ steps.version.outputs.short }}"
long = "${{ steps.version.outputs.long }}"
release_channel = "${{ inputs.release_channel }}"
tags = {
"beta": {
"debian": [
f"{short}", f"{long}", f"{short}-beta", f"{long}-beta", "beta", "latest",
f"{short}-debian", f"{long}-debian", f"{short}-beta-debian", f"{long}-beta-debian", "beta-debian"
],
"alpine": [
f"{short}-alpine", f"{long}-alpine", f"{short}-beta-alpine", f"{long}-beta-alpine", "beta-alpine"
]
},
"alpha": {
"debian": [
f"{short}-alpha", f"{long}-alpha", "alpha",
f"{short}-alpha-debian", f"{long}-alpha-debian", "alpha-debian"
],
"alpine": [
f"{short}-alpha-alpine", f"{long}-alpha-alpine", "alpha-alpine"
]
},
"daily": {
"debian": ["daily", "daily-debian"],
"alpine": ["daily-alpine"]
}
}
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
fh.write("debian<<EOF\n")
fh.write("\n".join(tags[release_channel]["debian"]))
fh.write("\nEOF\n")
fh.write("alpine<<EOF\n")
fh.write("\n".join(tags[release_channel]["alpine"]))
fh.write("\nEOF\n")
id: tags
- name: Docker login
uses: docker/login-action@v3
with:
username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Docker meta (Debian)
id: meta_debian
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
${{ steps.tags.outputs.debian }}
flavor: latest=false
- name: Create Docker manifest (Debian)
id: manifest_debian
uses: int128/docker-manifest-create-action@v2
with:
tags: |
${{ steps.meta_debian.outputs.tags }}
push: true
sources: |
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
- name: Docker meta (Alpine)
id: meta_alpine
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
${{ steps.tags.outputs.alpine }}
- name: Create Docker manifest (Alpine)
id: manifest_alpine
uses: int128/docker-manifest-create-action@v2
with:
tags: |
${{ steps.meta_alpine.outputs.tags }}
push: true
sources: |
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}

View File

@ -3,9 +3,7 @@ name: Trigger COPR build
on:
workflow_call:
secrets:
COPR_HOOK_DAILY:
COPR_HOOK_ALPHA:
COPR_HOOK_BETA:
COPR_API_CONFIG:
inputs:
copr_project:
description: COPR project to target
@ -27,35 +25,14 @@ jobs:
ref: ${{ github.ref }}
repository: ${{ github.repository }}
- name: Install Python dependencies
run: |
pip install requests
- name: Trigger COPR build
shell: python
run: |
import requests
project_name = "${{ inputs.copr_project }}"
if project_name == "daily":
hook_secret = "${{ secrets.COPR_HOOK_DAILY }}"
project_id = 160277
elif project_name == "alpha":
hook_secret = "${{ secrets.COPR_HOOK_ALPHA }}"
project_id = 160278
elif project_name == "beta":
hook_secret = "${{ secrets.COPR_HOOK_BETA }}"
project_id = 160279
else:
raise ValueError(f"Unknown COPR project: {project_name}")
webhook_url = f"https://copr.fedorainfracloud.org/webhooks/github/{project_id}/{hook_secret}/meshtasticd/"
copr_payload = {
"ref": "${{ github.ref }}",
"after": "${{ github.sha }}",
"repository": {
"clone_url": "${{ github.server_url }}/${{ github.repository }}.git",
}
}
r = requests.post(webhook_url, json=copr_payload, headers={"X-GitHub-Event": "push"})
r.raise_for_status()
uses: vidplace7/copr-build@main
id: copr_build
env:
COPR_API_TOKEN_CONFIG: ${{ secrets.COPR_API_CONFIG }}
with:
owner: "@meshtastic"
package-name: meshtasticd
project-name: ${{ inputs.copr_project }}
git-remote: "${{ github.server_url }}/${{ github.repository }}.git"
committish: ${{ github.sha }}

View File

@ -147,10 +147,37 @@ jobs:
test-native:
uses: ./.github/workflows/test_native.yml
build-docker:
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/build_docker.yml
secrets: inherit
docker-debian-amd64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-alpine-amd64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-debian-arm64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: false
docker-debian-armv7:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: false
after-checks:
runs-on: ubuntu-latest

View File

@ -9,6 +9,13 @@ permissions:
packages: write
jobs:
build-docker:
uses: ./.github/workflows/docker_manifest.yml
with:
release_channel: |-
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
secrets: inherit
package-ppa:
strategy:
fail-fast: false
@ -25,7 +32,7 @@ jobs:
uses: ./.github/workflows/package_obs.yml
with:
obs_project: |-
home:meshtastic:${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
network:Meshtastic:${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
series: |-
${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
secrets: inherit

3
.gitignore vendored
View File

@ -12,6 +12,9 @@ web.tar
*.code-workspace
.idea
.platformio
.local
.cache
.DS_Store
Thumbs.db

View File

@ -38,7 +38,8 @@ USER root
RUN apt-get update && apt-get --no-install-recommends -y install libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libulfius2.7 libusb-1.0-0-dev liborcania2.3 libssl3 && \
apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d
&& mkdir -p /etc/meshtasticd/config.d \
&& mkdir -p /etc/meshtasticd/ssl
# Fetch compiled binary from the builder
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/

View File

@ -29,7 +29,8 @@ USER root
RUN apk add libstdc++ libgpiod yaml-cpp libusb i2c-tools \
&& mkdir -p /var/lib/meshtasticd \
&& mkdir -p /etc/meshtasticd/config.d
&& mkdir -p /etc/meshtasticd/config.d \
&& mkdir -p /etc/meshtasticd/ssl
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/sbin/
WORKDIR /var/lib/meshtasticd

View File

@ -4,7 +4,7 @@ platform = platformio/nordicnrf52@^10.7.0
extends = arduino_base
platform_packages =
; our custom Git version until they merge our PR
framework-arduinoadafruitnrf52 @ https://github.com/geeksville/Adafruit_nRF52_Arduino.git
framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino.git#e13f5820002a4fb2a5e6754b42ace185277e5adf
toolchain-gccarmnoneeabi@~1.90301.0
build_type = debug

View File

@ -78,6 +78,8 @@ Lora:
# TXen: x # TX and RX enable pins
# RXen: x
# SX126X_MAX_POWER: 8 # Limit the output power to 8 dBm, useful for amped nodes
# spiSpeed: 2000000
### Set default/fallback gpio chip to use in /dev/. Defaults to 0.
@ -182,10 +184,12 @@ Logging:
Webserver:
# Port: 443 # Port for Webserver & Webservices
# RootPath: /usr/share/meshtasticd/web # Root Dir of WebServer
# SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present
# SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present
General:
MaxNodes: 200
MaxMessageQueue: 100
ConfigDirectory: /etc/meshtasticd/config.d/
# MACAddress: AA:BB:CC:DD:EE:FF
# MACAddressSource: eth0
# MACAddressSource: eth0

View File

@ -0,0 +1,9 @@
Lora:
Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header
## CS: 28
IRQ: 50
Busy: 62
Reset: 51
spidev: spidev1.0
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true

View File

@ -7,3 +7,6 @@ Lora:
TXen: 13
RXen: 12
DIO3_TCXO_VOLTAGE: true
# Only for E22-900M33S:
# Limit the output power to 8 dBm
# SX126X_MAX_POWER: 8

View File

@ -102,7 +102,7 @@ pref_flags = []
for pref in userPrefs:
if userPrefs[pref].startswith("{"):
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
elif userPrefs[pref].replace(".", "").isdigit():
elif userPrefs[pref].lstrip("-").replace(".", "").isdigit():
pref_flags.append("-D" + pref + "=" + userPrefs[pref])
elif userPrefs[pref] == "true" or userPrefs[pref] == "false":
pref_flags.append("-D" + pref + "=" + userPrefs[pref])

6
debian/changelog vendored
View File

@ -1,6 +1,8 @@
meshtasticd (2.5.20.0) UNRELEASED; urgency=medium
meshtasticd (2.5.21.0) UNRELEASED; urgency=medium
* Initial packaging
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
-- Austin Lane <github-actions[bot]@users.noreply.github.com> Mon, 13 Jan 2025 19:24:14 +0000
-- Austin Lane <github-actions[bot]@users.noreply.github.com> Sat, 25 Jan 2025 01:39:16 +0000

View File

@ -1,4 +1,5 @@
etc/meshtasticd
etc/meshtasticd/config.d
etc/meshtasticd/available.d
usr/share/meshtasticd/web
usr/share/meshtasticd/web
etc/meshtasticd/ssl

View File

@ -72,6 +72,8 @@ install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.s
# Install the web files under /usr/share/meshtasticd/web
mkdir -p %{buildroot}%{_datadir}/meshtasticd/web
cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
# Install default SSL storage directory (for web)
mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl
%files
%license LICENSE
@ -86,6 +88,7 @@ cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web
%dir %{_datadir}/meshtasticd
%dir %{_datadir}/meshtasticd/web
%{_datadir}/meshtasticd/web/*
%dir %{_sysconfdir}/meshtasticd/ssl
%changelog
%autochangelog

View File

@ -20,7 +20,7 @@ extra_scripts = bin/platformio-custom.py
build_flags = -Wno-missing-field-initializers
-Wno-format
-Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,.pio/build/output.map
-Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,"${platformio.build_dir}"/output.map
-DUSE_THREAD_NAMES
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
-DPB_ENABLE_MALLOC=1
@ -126,6 +126,7 @@ lib_deps =
mprograms/QMC5883LCompass@1.2.3
dfrobot/DFRobot_RTU@1.0.3
https://github.com/meshtastic/DFRobot_LarkWeatherStation#4de3a9cadef0f6a5220a8a906cf9775b02b0040d
https://github.com/DFRobot/DFRobot_RainfallSensor#38fea5e02b40a5430be6dab39a99a6f6347d667e
robtillaart/INA226@0.6.0
; Health Sensor Libraries

@ -1 +1 @@
Subproject commit 76f806e1bb1e2a7b157a14fadd095775f63db5e4
Subproject commit 7f13df0e5f7cbb07f0e6f3a57c0d86ad448738db

View File

@ -190,6 +190,20 @@ int32_t ButtonThread::runOnce()
case 4:
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
break;
#endif
#if defined(RAK_4631)
// 5 clicks: start accelerometer/magenetometer calibration for 30 seconds
case 5:
if (accelerometerThread) {
accelerometerThread->calibrate(30);
}
break;
// 6 clicks: start accelerometer/magenetometer calibration for 60 seconds
case 6:
if (accelerometerThread) {
accelerometerThread->calibrate(60);
}
break;
#endif
// No valid multipress action
default:

View File

@ -49,24 +49,6 @@ void OSFS::writeNBytes(uint16_t address, unsigned int num, const byte *input)
}
#endif
bool lfs_assert_failed =
false; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our modified lfs_util.h)
extern "C" void lfs_assert(const char *reason)
{
LOG_ERROR("LFS assert: %s", reason);
lfs_assert_failed = true;
#ifndef ARCH_PORTDUINO
#ifdef FSCom
// CORRUPTED FILESYSTEM. This causes bootloop so
// might as well try formatting now.
LOG_ERROR("Trying FSCom.format()");
FSCom.format();
#endif
#endif
}
/**
* @brief Copies a file from one location to another.
*
@ -203,7 +185,7 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
file.close();
}
} else {
meshtastic_FileInfo fileInfo = {"", file.size()};
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
#ifdef ARCH_ESP32
strcpy(fileInfo.file_name, file.path());
#else
@ -348,10 +330,16 @@ void rmDir(const char *dirname)
#endif
}
/**
* Some platforms (nrf52) might need to do an extra step before FSBegin().
*/
__attribute__((weak, noinline)) void preFSBegin() {}
void fsInit()
{
#ifdef FSCom
spiLock->lock();
concurrency::LockGuard g(spiLock);
preFSBegin();
if (!FSBegin()) {
LOG_ERROR("Filesystem mount failed");
// assert(0); This auto-formats the partition, so no need to fail here.
@ -362,7 +350,6 @@ void fsInit()
LOG_DEBUG("Filesystem files:");
#endif
listDir("/", 10);
spiLock->unlock();
#endif
}
@ -400,4 +387,4 @@ void setupSDCard()
LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024)));
LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024)));
#endif
}
}

View File

@ -57,7 +57,4 @@ bool renameFile(const char *pathFrom, const char *pathTo);
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
void listDir(const char *dirname, uint8_t levels, bool del = false);
void rmDir(const char *dirname);
void setupSDCard();
extern bool lfs_assert_failed; // Note: we use this global on all platforms, though it can only be set true on nrf52 (in our
// modified lfs_util.h)
void setupSDCard();

View File

@ -87,7 +87,7 @@ MAX17048Sensor max17048Sensor;
#endif
#endif
#if HAS_RAKPROT && !defined(ARCH_PORTDUINO)
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT && !defined(ARCH_PORTDUINO)
RAK9154Sensor rak9154Sensor;
#endif
@ -243,7 +243,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
virtual uint16_t getBattVoltage() override
{
#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU)
#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU) && \
!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
if (hasRAK()) {
return getRAKVoltage();
}
@ -406,7 +407,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
/// we can't be smart enough to say 'full'?
virtual bool isCharging() override
{
#if defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && !defined(HAS_PMU)
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(ARCH_PORTDUINO) && \
!defined(HAS_PMU)
if (hasRAK()) {
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
}
@ -447,7 +449,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
uint32_t last_read_time_ms = 0;
#if defined(HAS_RAKPROT)
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); }

View File

@ -79,17 +79,17 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
}
if (color && logLevel != nullptr) {
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0)
Print::write("\u001b[34m", 6);
Print::write("\u001b[34m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0)
Print::write("\u001b[32m", 6);
Print::write("\u001b[32m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0)
Print::write("\u001b[33m", 6);
Print::write("\u001b[33m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0)
Print::write("\u001b[31m", 6);
Print::write("\u001b[31m", 5);
}
len = Print::write(printBuf, len);
if (color && logLevel != nullptr) {
Print::write("\u001b[0m", 5);
Print::write("\u001b[0m", 4);
}
return len;
}
@ -107,15 +107,15 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
// include the header
if (color) {
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0)
Print::write("\u001b[34m", 6);
Print::write("\u001b[34m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0)
Print::write("\u001b[32m", 6);
Print::write("\u001b[32m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0)
Print::write("\u001b[33m", 6);
Print::write("\u001b[33m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0)
Print::write("\u001b[31m", 6);
Print::write("\u001b[31m", 5);
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0)
Print::write("\u001b[35m", 6);
Print::write("\u001b[35m", 5);
}
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // display local time on logfile
@ -393,4 +393,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...)
break;
}
return std::string(formatted.get());
}
}

View File

@ -8,10 +8,8 @@ static File openFile(const char *filename, bool fullAtomic)
concurrency::LockGuard g(spiLock);
LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic);
#ifdef ARCH_NRF52
lfs_assert_failed = false;
File file = FSCom.open(filename, FILE_O_WRITE);
file.seek(0);
return file;
FSCom.remove(filename);
return FSCom.open(filename, FILE_O_WRITE);
#endif
if (!fullAtomic)
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
@ -20,7 +18,6 @@ static File openFile(const char *filename, bool fullAtomic)
filenameTmp += ".tmp";
// clear any previous LFS errors
lfs_assert_failed = false;
return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE);
}
@ -61,9 +58,6 @@ bool SafeFile::close()
return false;
spiLock->lock();
#ifdef ARCH_NRF52
f.truncate();
#endif
f.close();
spiLock->unlock();
@ -96,8 +90,6 @@ bool SafeFile::close()
bool SafeFile::testReadback()
{
concurrency::LockGuard g(spiLock);
bool lfs_failed = lfs_assert_failed;
lfs_assert_failed = false;
String filenameTmp = filename;
filenameTmp += ".tmp";
@ -119,7 +111,7 @@ bool SafeFile::testReadback()
return false;
}
return !lfs_failed;
return true;
}
#endif

View File

@ -145,6 +145,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define OPT3001_ADDR_ALT 0x44
#define MLX90632_ADDR 0x3A
#define DFROBOT_LARK_ADDR 0x42
#define DFROBOT_RAIN_ADDR 0x1d
#define NAU7802_ADDR 0x2A
#define MAX30102_ADDR 0x57
#define MLX90614_ADDR_DEF 0x5A

View File

@ -66,6 +66,7 @@ class ScanI2C
CGRADSENS,
INA226,
NXP_SE050,
DFROBOT_RAIN,
} DeviceType;
// typedef uint8_t DeviceAddress;

View File

@ -84,23 +84,33 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
return o_probe;
}
uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation &registerLocation,
ScanI2CTwoWire::ResponseWidth responseWidth) const
ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const
{
uint16_t value = 0x00;
TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress);
i2cBus->beginTransmission(registerLocation.i2cAddress.address);
i2cBus->write(registerLocation.registerAddress);
if (zeropad) {
// Lark Commands need the argument list length in 2 bytes.
i2cBus->write((int)0);
i2cBus->write((int)0);
}
i2cBus->endTransmission();
delay(20);
i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth);
if (i2cBus->available() == 2) {
if (i2cBus->available() > 1) {
// Read MSB, then LSB
value = (uint16_t)i2cBus->read() << 8;
value |= i2cBus->read();
} else if (i2cBus->available()) {
value = i2cBus->read();
}
// Drain excess bytes
for (uint8_t i = 0; i < responseWidth - 1; i++) {
if (i2cBus->available())
i2cBus->read();
}
return value;
}
@ -286,7 +296,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
RESPONSE_PAYLOAD 0x01
RESPONSE_PAYLOAD+1 0x00
*/
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 2);
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true);
LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue);
if (registerValue == 0x5305) {
logFoundDevice("DFRobot Lark", (uint8_t)addr.address);
@ -402,6 +412,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
#ifdef HAS_TPS65233
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
#endif

View File

@ -53,7 +53,7 @@ class ScanI2CTwoWire : public ScanI2C
concurrency::Lock lock;
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth) const;
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
DeviceType probeOLED(ScanI2C::DeviceAddress) const;

View File

@ -123,7 +123,7 @@ static bool heartbeat = false;
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
/// Check if the display can render a string (detect special chars; emoji)
// Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str)
{
#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS)
@ -162,11 +162,7 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl
display->setFont(FONT_MEDIUM);
display->setTextAlignment(TEXT_ALIGN_LEFT);
#ifdef USERPREFS_SPLASH_TITLE
const char *title = USERPREFS_SPLASH_TITLE;
#else
const char *title = "meshtastic.org";
#endif
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
display->setFont(FONT_SMALL);
@ -185,6 +181,56 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
}
#ifdef USERPREFS_OEM_TEXT
static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA;
display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH,
USERPREFS_OEM_IMAGE_HEIGHT, xbm);
switch (USERPREFS_OEM_FONT_SIZE) {
case 0:
display->setFont(FONT_SMALL);
break;
case 2:
display->setFont(FONT_LARGE);
break;
default:
display->setFont(FONT_MEDIUM);
break;
}
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *title = USERPREFS_OEM_TEXT;
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
display->setFont(FONT_SMALL);
// Draw region in upper left
if (upperMsg)
display->drawString(x + 0, y + 0, upperMsg);
// Draw version and shortname in upper right
char buf[25];
snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : "");
display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(x + SCREEN_WIDTH, y + 0, buf);
screen->forceDisplay();
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
}
static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Draw region in upper left
const char *region = myRegion ? myRegion->name : NULL;
drawOEMIconScreen(region, display, state, x, y);
}
#endif
void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message)
{
uint16_t x_offset = display->width() / 2;
@ -1658,6 +1704,10 @@ void Screen::setup()
// Set the utf8 conversion function
dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Double the time if we have a custom logo
#endif
// Add frames.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST);
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
@ -1803,6 +1853,22 @@ int32_t Screen::runOnce()
showingBootScreen = false;
}
#ifdef USERPREFS_OEM_TEXT
static bool showingOEMBootScreen = true;
if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) {
LOG_INFO("Switch to OEM screen...");
// Change frames.
static FrameCallback bootOEMFrames[] = {drawOEMBootScreen};
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
ui->update();
#ifndef USE_EINK
ui->update();
#endif
showingOEMBootScreen = false;
}
#endif
#ifndef DISABLE_WELCOME_UNSET
if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
setWelcomeFrames();

View File

@ -278,6 +278,10 @@ class Screen : public concurrency::OSThread
bool hasHeading() { return hasCompass; }
long getHeading() { return compassHeading; }
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
uint32_t getEndCalibration() { return endCalibrationAt; }
// functions for display brightness
void increaseBrightness();
void decreaseBrightness();
@ -673,6 +677,8 @@ class Screen : public concurrency::OSThread
bool hasCompass = false;
float compassHeading;
uint32_t endCalibrationAt;
/// Holds state for debug information
DebugInfo debugInfo;

View File

@ -16,63 +16,61 @@
#include "graphics/fonts/OLEDDisplayFontsCS.h"
#endif
#ifdef OLED_PL
#define FONT_SMALL_LOCAL ArialMT_Plain_10_PL
#else
#ifdef OLED_RU
#define FONT_SMALL_LOCAL ArialMT_Plain_10_RU
#else
#ifdef OLED_UA
#define FONT_SMALL_LOCAL ArialMT_Plain_10_UA // Height: 13
#else
#ifdef OLED_CS
#define FONT_SMALL_LOCAL ArialMT_Plain_10_CS
#else
#define FONT_SMALL_LOCAL ArialMT_Plain_10 // Height: 13
#endif
#endif
#endif
#endif
#ifdef OLED_PL
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
#else
#ifdef OLED_UA
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
#else
#ifdef OLED_CS
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_CS
#else
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16 // Height: 19
#endif
#endif
#endif
#ifdef OLED_PL
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
#else
#ifdef OLED_UA
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
#else
#ifdef OLED_CS
#define FONT_LARGE_LOCAL ArialMT_Plain_24_CS // Height: 28
#else
#define FONT_LARGE_LOCAL ArialMT_Plain_24 // Height: 28
#endif
#endif
#endif
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts
#ifdef OLED_PL
#define FONT_SMALL ArialMT_Plain_16_PL // Height: 19
#define FONT_MEDIUM ArialMT_Plain_24_PL // Height: 28
#define FONT_LARGE ArialMT_Plain_24_PL // Height: 28
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#else
#define FONT_SMALL ArialMT_Plain_16 // Height: 19
#define FONT_MEDIUM ArialMT_Plain_24 // Height: 28
#define FONT_LARGE ArialMT_Plain_24 // Height: 28
#endif
#else
#ifdef OLED_PL
#define FONT_SMALL ArialMT_Plain_10_PL
#else
#ifdef OLED_RU
#define FONT_SMALL ArialMT_Plain_10_RU
#else
#ifdef OLED_UA
#define FONT_SMALL ArialMT_Plain_10_UA // Height: 13
#else
#ifdef OLED_CS
#define FONT_SMALL ArialMT_Plain_10_CS
#else
#define FONT_SMALL ArialMT_Plain_10 // Height: 13
#endif
#endif
#endif
#endif
#ifdef OLED_PL
#define FONT_MEDIUM ArialMT_Plain_16_PL // Height: 19
#else
#ifdef OLED_UA
#define FONT_MEDIUM ArialMT_Plain_16_UA // Height: 19
#else
#ifdef OLED_CS
#define FONT_MEDIUM ArialMT_Plain_16_CS
#else
#define FONT_MEDIUM ArialMT_Plain_16 // Height: 19
#endif
#endif
#endif
#ifdef OLED_PL
#define FONT_LARGE ArialMT_Plain_24_PL // Height: 28
#else
#ifdef OLED_UA
#define FONT_LARGE ArialMT_Plain_24_UA // Height: 28
#else
#ifdef OLED_CS
#define FONT_LARGE ArialMT_Plain_24_CS // Height: 28
#else
#define FONT_LARGE ArialMT_Plain_24 // Height: 28
#endif
#endif
#endif
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
#define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#endif
#define _fontHeight(font) ((font)[1] + 1) // height is position 1

View File

@ -7,6 +7,9 @@
#include "ScanAndSelect.h"
#include "modules/CannedMessageModule.h"
#include <Throttle.h>
#ifdef ARCH_PORTDUINO // Only to check for pin conflict with user button
#include "platform/portduino/PortduinoGlue.h"
#endif
// Config
static const char name[] = "scanAndSelect"; // should match "allow input source" string
@ -30,7 +33,9 @@ bool ScanAndSelectInput::init()
if (strcasecmp(moduleConfig.canned_message.allow_input_source, name) != 0)
return false;
// Use any available inputbroker pin as the button
// Determine which pin to use for the single scan-and-select button
// User can specify this by setting any of the inputbroker pins
// If all values are zero, we'll assume the user *does* want GPIO0
if (moduleConfig.canned_message.inputbroker_pin_press)
pin = moduleConfig.canned_message.inputbroker_pin_press;
else if (moduleConfig.canned_message.inputbroker_pin_a)
@ -38,7 +43,25 @@ bool ScanAndSelectInput::init()
else if (moduleConfig.canned_message.inputbroker_pin_b)
pin = moduleConfig.canned_message.inputbroker_pin_b;
else
return false; // Short circuit: no button found
pin = 0; // GPIO 0 then
// Short circuit: if selected pin conficts with the user button
#if defined(ARCH_PORTDUINO)
int pinUserButton = 0;
if (settingsMap.count(user) != 0) {
pinUserButton = settingsMap[user];
}
#elif defined(USERPREFS_BUTTON_PIN)
int pinUserButton = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
#elif defined(BUTTON_PIN)
int pinUserButton = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#else
int pinUserButton = config.device.button_gpio;
#endif
if (pin == pinUserButton) {
LOG_ERROR("ScanAndSelect conflict with user button");
return false;
}
// Set-up the button
pinMode(pin, INPUT_PULLUP);

View File

@ -115,10 +115,6 @@ AccelerometerThread *accelerometerThread = nullptr;
AudioThread *audioThread = nullptr;
#endif
#if defined(TCXO_OPTIONAL)
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
#endif
using namespace concurrency;
volatile static const char slipstreamTZString[] = USERPREFS_TZ_STRING;
@ -610,6 +606,7 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::CGRADSENS, meshtastic_TelemetrySensorType_RADSENS);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN, meshtastic_TelemetrySensorType_DFROBOT_RAIN);
i2cScanner.reset();
#endif
@ -927,13 +924,16 @@ void setup()
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
#endif
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio");
delete rIf;
rIf = NULL;
delete sxIf;
} else {
LOG_INFO("SX1262 init success");
rIf = sxIf;
radioType = SX1262_RADIO;
}
}
@ -941,29 +941,28 @@ void setup()
#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL)
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// Try using the specified TCXO voltage
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No SX1262 radio with TCXO, Vref %f V", tcxoVoltage);
delete rIf;
rIf = NULL;
tcxoVoltage = 0; // if it fails, set the TCXO voltage to zero for the next attempt
// try using the specified TCXO voltage
auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE);
if (!sxIf->init()) {
LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
delete sxIf;
} else {
LOG_WARN("SX1262 init success, TCXO, Vref %f V", tcxoVoltage);
LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE);
rIf = sxIf;
radioType = SX1262_RADIO;
}
}
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instea
// If specified TCXO voltage fails, attempt to use DIO3 as a reference instead
rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY);
if (!rIf->init()) {
LOG_WARN("No SX1262 radio with XTAL, Vref %f V", tcxoVoltage);
LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V");
delete rIf;
rIf = NULL;
tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if it fails, set the TCXO voltage back for the next radio search
} else {
LOG_INFO("SX1262 init success, XTAL, Vref %f V", tcxoVoltage);
LOG_INFO("SX1262 init success, XTAL, Vref 0.0V");
radioType = SX1262_RADIO;
}
}

View File

@ -20,12 +20,18 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
// specified (may be dangerous if using external PA and LR11x0 power config forgotten)
#if ARCH_PORTDUINO
#define LR1110_MAX_POWER settingsMap[lr1110_max_power]
#endif
#ifndef LR1110_MAX_POWER
#define LR1110_MAX_POWER 22
#endif
// the 2.4G part maxes at 13dBm
#if ARCH_PORTDUINO
#define LR1120_MAX_POWER settingsMap[lr1120_max_power]
#endif
#ifndef LR1120_MAX_POWER
#define LR1120_MAX_POWER 13
#endif

View File

@ -10,6 +10,7 @@
std::vector<MeshModule *> *MeshModule::modules;
const meshtastic_MeshPacket *MeshModule::currentRequest;
uint8_t MeshModule::numPeriodicModules = 0;
/**
* If any of the current chain of modules has already sent a reply, it will be here. This is useful to allow
@ -35,6 +36,15 @@ MeshModule::~MeshModule()
modules->erase(it);
}
// ⚠️ **Only call once** to set the initial delay before a module starts broadcasting periodically
int32_t MeshModule::setStartDelay()
{
int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS;
numPeriodicModules++;
return startDelay;
}
meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
uint8_t hopLimit)
{

View File

@ -9,6 +9,9 @@
#include <OLEDDisplayUi.h>
#endif
#define MESHMODULE_MIN_BROADCAST_DELAY_MS 30 * 1000 // Min. delay after boot before sending first broadcast by any module
#define MESHMODULE_BROADCAST_SPACING_MS 15 * 1000 // Initial spacing between broadcasts of different modules
/** handleReceived return enumeration
*
* Use ProcessMessage::CONTINUE to allows other modules to process a message.
@ -119,6 +122,12 @@ class MeshModule
*/
static const meshtastic_MeshPacket *currentRequest;
// We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time
static uint8_t numPeriodicModules;
// Set the start delay for module that broadcasts periodically
int32_t setStartDelay();
/**
* If your handler wants to send a response, simply set currentReply and it will be sent at the end of response handling.
*/

View File

@ -64,7 +64,8 @@ class MeshService
return true;
}
return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP ||
p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP;
p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP ||
p->decoded.portnum == meshtastic_PortNum_ALERT_APP;
}
/// Called when some new packets have arrived from one of the radios
Observable<uint32_t> fromNumChanged;

View File

@ -197,9 +197,8 @@ NodeDB::NodeDB()
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
int saveWhat = 0;
// bool hasUniqueId = false;
// Get device unique id
#if defined(ARCH_ESP32) && defined(ESP_EFUSE_OPTIONAL_UNIQUE_ID)
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)
uint32_t unique_id[4];
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
@ -207,7 +206,6 @@ NodeDB::NodeDB()
if (err == ESP_OK) {
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
myNodeInfo.device_id.size = 16;
hasUniqueId = true;
} else {
LOG_WARN("Failed to read unique id from efuse");
}
@ -221,12 +219,12 @@ NodeDB::NodeDB()
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
myNodeInfo.device_id.size = 16;
// Uncomment below to print the device id
// hasUniqueId = true;
#else
// FIXME - implement for other platforms
#endif
// if (hasUniqueId) {
// if (myNodeInfo.device_id.size == 16) {
// std::string deviceIdHex;
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
// char buf[3];

View File

@ -4,7 +4,11 @@
#include <unordered_set>
/// We clear our old flood record 10 minutes after we see the last of it
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
#define FLOOD_EXPIRE_TIME (5 * 1000L) // Don't allow too many packets to accumulate when fuzzing.
#else
#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L)
#endif
#define NUM_RELAYERS \
3 // Number of relayer we keep track of. Use 3 to be efficient with memory alignment of PacketRecord to 16 bytes

View File

@ -9,6 +9,9 @@
#include "PortduinoGlue.h"
#endif
#if ARCH_PORTDUINO
#define RF95_MAX_POWER settingsMap[rf95_max_power]
#endif
#ifndef RF95_MAX_POWER
#define RF95_MAX_POWER 20
#endif
@ -337,4 +340,4 @@ bool RF95Interface::sleep()
return true;
}
#endif
#endif

View File

@ -346,8 +346,11 @@ void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id)
meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false);
if (p) {
p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr);
txQueue.enqueue(p);
LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis());
if (txQueue.enqueue(p)) {
LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis());
} else {
packetPool.release(p);
}
}
}

View File

@ -11,6 +11,9 @@
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
// specified (may be dangerous if using external PA and SX126x power config forgotten)
#if ARCH_PORTDUINO
#define SX126X_MAX_POWER settingsMap[sx126x_max_power]
#endif
#ifndef SX126X_MAX_POWER
#define SX126X_MAX_POWER 22
#endif
@ -50,22 +53,13 @@ template <typename T> bool SX126xInterface<T>::init()
#endif
#if ARCH_PORTDUINO
float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000;
tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000;
if (settingsMap[sx126x_ant_sw_pin] != RADIOLIB_NC) {
digitalWrite(settingsMap[sx126x_ant_sw_pin], HIGH);
pinMode(settingsMap[sx126x_ant_sw_pin], OUTPUT);
}
// FIXME: correct logic to default to not using TCXO if no voltage is specified for SX126X_DIO3_TCXO_VOLTAGE
#elif !defined(SX126X_DIO3_TCXO_VOLTAGE)
float tcxoVoltage =
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.h#L471C26-L471C104
// (DIO3 is free to be used as an IRQ)
#elif !defined(TCXO_OPTIONAL)
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE;
// (DIO3 is not free to be used as an IRQ)
#endif
if (tcxoVoltage == 0)
if (tcxoVoltage == 0.0)
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
else
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage);
@ -83,7 +77,7 @@ template <typename T> bool SX126xInterface<T>::init()
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO);
// \todo Display actual typename of the adapter, not just `SX126x`
LOG_INFO("SX126x init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND)
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
return false;
LOG_INFO("Frequency set to %f", getFreq());
@ -342,4 +336,4 @@ template <typename T> bool SX126xInterface<T>::sleep()
return true;
}
#endif
#endif

View File

@ -28,8 +28,11 @@ template <class T> class SX126xInterface : public RadioLibInterface
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; }
protected:
float currentLimit = 140; // Higher OCP limit for SX126x PA
float tcxoVoltage = 0.0;
/**
* Specific module instance

View File

@ -10,6 +10,9 @@
#endif
// Particular boards might define a different max power based on what their hardware can do
#if ARCH_PORTDUINO
#define SX128X_MAX_POWER settingsMap[sx128x_max_power]
#endif
#ifndef SX128X_MAX_POWER
#define SX128X_MAX_POWER 13
#endif
@ -315,4 +318,4 @@ template <typename T> bool SX128xInterface<T>::sleep()
return true;
}
#endif
#endif

View File

@ -74,11 +74,17 @@ template <class T> class TypedQueue
{
std::queue<T> q;
concurrency::OSThread *reader = NULL;
int maxElements;
public:
explicit TypedQueue(int maxElements) {}
explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {}
int numFree() { return 1; } // Always claim 1 free, because we can grow to any size
int numFree()
{
if (maxElements <= 0)
return 1; // Always claim 1 free, because we can grow to any size
return maxElements - numUsed();
}
bool isEmpty() { return q.empty(); }
@ -86,6 +92,9 @@ template <class T> class TypedQueue
bool enqueue(T x, TickType_t maxWait = portMAX_DELAY)
{
if (numFree() <= 0)
return false;
if (reader) {
reader->setInterval(0);
concurrency::mainDelay.interrupt();
@ -112,4 +121,4 @@ template <class T> class TypedQueue
void setReader(concurrency::OSThread *t) { reader = t; }
};
#endif
#endif

View File

@ -223,6 +223,9 @@ typedef enum _meshtastic_HardwareModel {
/* Mesh-Tab, esp32 based
https://github.com/valzzu/Mesh-Tab */
meshtastic_HardwareModel_MESH_TAB = 86,
/* MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog
https://www.loraitalia.it */
meshtastic_HardwareModel_MESHLINK = 87,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */

View File

@ -81,7 +81,9 @@ typedef enum _meshtastic_TelemetrySensorType {
/* ClimateGuard RadSens, radiation, Geiger-Muller Tube */
meshtastic_TelemetrySensorType_RADSENS = 33,
/* High accuracy current and voltage */
meshtastic_TelemetrySensorType_INA226 = 34
meshtastic_TelemetrySensorType_INA226 = 34,
/* DFRobot Gravity tipping bucket rain gauge */
meshtastic_TelemetrySensorType_DFROBOT_RAIN = 35
} meshtastic_TelemetrySensorType;
/* Struct definitions */
@ -162,6 +164,12 @@ typedef struct _meshtastic_EnvironmentMetrics {
/* Radiation in µR/h */
bool has_radiation;
float radiation;
/* Rainfall in the last hour in mm */
bool has_rainfall_1h;
float rainfall_1h;
/* Rainfall in the last 24 hours in mm */
bool has_rainfall_24h;
float rainfall_24h;
} meshtastic_EnvironmentMetrics;
/* Power Metrics (voltage / current / etc) */
@ -306,8 +314,8 @@ extern "C" {
/* Helper constants for enums */
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_INA226
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_INA226+1))
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_DFROBOT_RAIN
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_DFROBOT_RAIN+1))
@ -320,7 +328,7 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
@ -328,7 +336,7 @@ extern "C" {
#define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}}
#define meshtastic_Nau7802Config_init_default {0, 0}
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
@ -360,6 +368,8 @@ extern "C" {
#define meshtastic_EnvironmentMetrics_wind_gust_tag 16
#define meshtastic_EnvironmentMetrics_wind_lull_tag 17
#define meshtastic_EnvironmentMetrics_radiation_tag 18
#define meshtastic_EnvironmentMetrics_rainfall_1h_tag 19
#define meshtastic_EnvironmentMetrics_rainfall_24h_tag 20
#define meshtastic_PowerMetrics_ch1_voltage_tag 1
#define meshtastic_PowerMetrics_ch1_current_tag 2
#define meshtastic_PowerMetrics_ch2_voltage_tag 3
@ -431,7 +441,9 @@ X(a, STATIC, OPTIONAL, FLOAT, wind_speed, 14) \
X(a, STATIC, OPTIONAL, FLOAT, weight, 15) \
X(a, STATIC, OPTIONAL, FLOAT, wind_gust, 16) \
X(a, STATIC, OPTIONAL, FLOAT, wind_lull, 17) \
X(a, STATIC, OPTIONAL, FLOAT, radiation, 18)
X(a, STATIC, OPTIONAL, FLOAT, radiation, 18) \
X(a, STATIC, OPTIONAL, FLOAT, rainfall_1h, 19) \
X(a, STATIC, OPTIONAL, FLOAT, rainfall_24h, 20)
#define meshtastic_EnvironmentMetrics_CALLBACK NULL
#define meshtastic_EnvironmentMetrics_DEFAULT NULL
@ -530,12 +542,12 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size
#define meshtastic_AirQualityMetrics_size 78
#define meshtastic_DeviceMetrics_size 27
#define meshtastic_EnvironmentMetrics_size 91
#define meshtastic_EnvironmentMetrics_size 103
#define meshtastic_HealthMetrics_size 11
#define meshtastic_LocalStats_size 60
#define meshtastic_Nau7802Config_size 16
#define meshtastic_PowerMetrics_size 30
#define meshtastic_Telemetry_size 98
#define meshtastic_Telemetry_size 110
#ifdef __cplusplus
} /* extern "C" */

View File

@ -65,6 +65,9 @@ mail: marchammermann@googlemail.com
#define DEFAULT_REALM "default_realm"
#define PREFIX ""
#define KEY_PATH settingsStrings[websslkeypath].c_str()
#define CERT_PATH settingsStrings[websslcertpath].c_str()
struct _file_config configWeb;
// We need to specify some content-type mapping, so the resources get delivered with the
@ -384,13 +387,13 @@ char *read_file_into_string(const char *filename)
int PiWebServerThread::CheckSSLandLoad()
{
// read certificate
cert_pem = read_file_into_string("certificate.pem");
cert_pem = read_file_into_string(CERT_PATH);
if (cert_pem == NULL) {
LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing");
return 1;
}
// read private key
key_pem = read_file_into_string("private_key.pem");
key_pem = read_file_into_string(KEY_PATH);
if (key_pem == NULL) {
LOG_ERROR("ERROR file private_key can't be loaded or is missing");
return 2;
@ -415,8 +418,8 @@ int PiWebServerThread::CreateSSLCertificate()
return 2;
}
// Ope file to write private key file
FILE *pkey_file = fopen("private_key.pem", "wb");
// Open file to write private key file
FILE *pkey_file = fopen(KEY_PATH, "wb");
if (!pkey_file) {
LOG_ERROR("Error opening private key file");
return 3;
@ -426,18 +429,19 @@ int PiWebServerThread::CreateSSLCertificate()
fclose(pkey_file);
// open Certificate file
FILE *x509_file = fopen("certificate.pem", "wb");
FILE *x509_file = fopen(CERT_PATH, "wb");
if (!x509_file) {
LOG_ERROR("Error opening cert");
return 4;
}
// write cirtificate
// write certificate
PEM_write_X509(x509_file, x509);
fclose(x509_file);
EVP_PKEY_free(pkey);
LOG_INFO("Create SSL Key %s successful", KEY_PATH);
X509_free(x509);
LOG_INFO("Create SSL Cert -certificate.pem- succesfull ");
LOG_INFO("Create SSL Cert %s successful", CERT_PATH);
return 0;
}

View File

@ -225,7 +225,7 @@ bool initWifi()
#if !MESHTASTIC_EXCLUDE_WEBSERVER
createSSLCert(); // For WebServer
#endif
esp_wifi_set_storage(WIFI_STORAGE_RAM); // Disable flash storage for WiFi credentials
WiFi.persistent(false); // Disable flash storage for WiFi credentials
#endif
if (!*wifiPsw) // Treat empty password as no password
wifiPsw = NULL;

View File

@ -81,7 +81,7 @@ int32_t DetectionSensorModule::runOnce()
}
LOG_INFO("Detection Sensor Module: init");
return DELAYED_INTERVAL;
return setStartDelay();
}
// LOG_DEBUG("Detection Sensor Module: Current pin state: %i", digitalRead(moduleConfig.detection_sensor.monitor_pin));
@ -161,4 +161,4 @@ bool DetectionSensorModule::hasDetectionEvent()
bool currentState = digitalRead(moduleConfig.detection_sensor.monitor_pin);
// LOG_DEBUG("Detection Sensor Module: Current state: %i", currentState);
return (moduleConfig.detection_sensor.detection_trigger_type & 1) ? currentState : !currentState;
}
}

View File

@ -97,8 +97,9 @@ NodeInfoModule::NodeInfoModule()
: ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo")
{
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
setIntervalFromNow(30 *
1000); // Send our initial owner announcement 30 seconds after we start (to give network time to setup)
setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds
// after we start (to give network time to setup)
}
int32_t NodeInfoModule::runOnce()
@ -112,4 +113,4 @@ int32_t NodeInfoModule::runOnce()
sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies)
}
return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs);
}
}

View File

@ -28,8 +28,9 @@ PositionModule::PositionModule()
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
setIntervalFromNow(60 * 1000);
config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
setIntervalFromNow(setStartDelay());
}
// Power saving trackers should clear their position on startup to avoid waking up and sending a stale position
if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
@ -160,7 +161,8 @@ bool PositionModule::hasGPS()
#endif
}
meshtastic_MeshPacket *PositionModule::allocReply()
// Allocate a packet with our position data if we have one
meshtastic_MeshPacket *PositionModule::allocPositionPacket()
{
if (precision == 0) {
LOG_DEBUG("Skip location send because precision is set to 0!");
@ -262,7 +264,8 @@ meshtastic_MeshPacket *PositionModule::allocReply()
p.has_ground_speed = true;
}
LOG_INFO("Position reply: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i);
lastSentToMesh = millis();
// TAK Tracker devices should send their position in a TAK packet over the ATAK port
if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)
@ -271,6 +274,16 @@ meshtastic_MeshPacket *PositionModule::allocReply()
return allocDataProtobuf(p);
}
meshtastic_MeshPacket *PositionModule::allocReply()
{
if (lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) {
LOG_DEBUG("Skip Position reply since we sent it <3min ago");
ignoreRequest = true; // Mark it as ignored for MeshModule
return nullptr;
}
return allocPositionPacket();
}
meshtastic_MeshPacket *PositionModule::allocAtakPli()
{
LOG_INFO("Send TAK PLI packet");
@ -333,9 +346,9 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha
precision = 0;
}
meshtastic_MeshPacket *p = allocReply();
meshtastic_MeshPacket *p = allocPositionPacket();
if (p == nullptr) {
LOG_DEBUG("allocReply returned a nullptr");
LOG_DEBUG("allocPositionPacket returned a nullptr");
return;
}

View File

@ -55,6 +55,7 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
virtual int32_t runOnce() override;
private:
meshtastic_MeshPacket *allocPositionPacket();
struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition);
meshtastic_MeshPacket *allocAtakPli();
void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false);
@ -62,6 +63,7 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
void sendLostAndFoundText();
bool hasQualityTimesource();
bool hasGPS();
uint32_t lastSentToMesh = 0; // Last time we sent our position to the mesh
const uint32_t minimumTimeThreshold =
Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30);

View File

@ -50,12 +50,12 @@ int32_t AirQualityTelemetryModule::runOnce()
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address);
return 1000;
return setStartDelay();
}
#endif
return disable();
}
return 1000;
return setStartDelay();
}
return disable();
} else {

View File

@ -18,7 +18,7 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu
uptimeWrapCount = 0;
uptimeLastMs = millis();
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
setIntervalFromNow(45 * 1000); // Wait until NodeInfo is sent
setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent
}
virtual bool wantUIFrame() { return false; }
@ -62,4 +62,4 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu
uint32_t uptimeWrapCount;
uint32_t uptimeLastMs;
};
};

View File

@ -27,6 +27,7 @@
#include "Sensor/BMP280Sensor.h"
#include "Sensor/BMP3XXSensor.h"
#include "Sensor/CGRadSensSensor.h"
#include "Sensor/DFRobotGravitySensor.h"
#include "Sensor/DFRobotLarkSensor.h"
#include "Sensor/LPS22HBSensor.h"
#include "Sensor/MCP9808Sensor.h"
@ -56,6 +57,7 @@ RCWL9620Sensor rcwl9620Sensor;
AHT10Sensor aht10Sensor;
MLX90632Sensor mlx90632Sensor;
DFRobotLarkSensor dfRobotLarkSensor;
DFRobotGravitySensor dfRobotGravitySensor;
NAU7802Sensor nau7802Sensor;
BMP3XXSensor bmp3xxSensor;
CGRadSensSensor cgRadSens;
@ -105,8 +107,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
if (moduleConfig.telemetry.environment_measurement_enabled) {
LOG_INFO("Environment Telemetry: init");
// it's possible to have this module enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
#ifdef SENSECAP_INDICATOR
result = indicatorSensor.runOnce();
#endif
@ -115,6 +115,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
#elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
if (dfRobotLarkSensor.hasSensor())
result = dfRobotLarkSensor.runOnce();
if (dfRobotGravitySensor.hasSensor())
result = dfRobotGravitySensor.runOnce();
if (bmp085Sensor.hasSensor())
result = bmp085Sensor.runOnce();
if (bmp280Sensor.hasSensor())
@ -159,9 +161,17 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = max17048Sensor.runOnce();
if (cgRadSens.hasSensor())
result = cgRadSens.runOnce();
// this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
// sensormap here.
#ifdef HAS_RAKPROT
result = rak9154Sensor.runOnce();
#endif
#endif
}
return result;
// it's possible to have this module enabled, only for displaying values on the screen.
// therefore, we should only enable the sensor loop if measurement is also enabled
return result == UINT32_MAX ? disable() : setStartDelay();
} else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.environment_measurement_enabled) {
@ -278,8 +288,18 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
static bool scrollingDown = true;
static uint32_t lastScrollTime = millis();
// Draw up to 3 sensor data lines
int linesToShow = min(3, sensorCount);
// Determine how many lines we can fit on display
// Calculated once only: display dimensions don't change during runtime.
static int maxLines = 0;
if (!maxLines) {
const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text
const int16_t paddingBottom = 8; // Indicator dots
maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL);
assert(maxLines > 0);
}
// Draw as many lines of data as we can fit
int linesToShow = min(maxLines, sensorCount);
for (int i = 0; i < linesToShow; i++) {
int index = (scrollOffset + i) % sensorCount;
display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]);
@ -358,6 +378,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
valid = valid && dfRobotLarkSensor.getMetrics(m);
hasSensor = true;
}
if (dfRobotGravitySensor.hasSensor()) {
valid = valid && dfRobotGravitySensor.getMetrics(m);
hasSensor = true;
}
if (sht31Sensor.hasSensor()) {
valid = valid && sht31Sensor.getMetrics(m);
hasSensor = true;
@ -462,6 +486,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
valid = valid && cgRadSens.getMetrics(m);
hasSensor = true;
}
#ifdef HAS_RAKPROT
valid = valid && rak9154Sensor.getMetrics(m);
hasSensor = true;
#endif
#endif
return valid && hasSensor;
}
@ -559,6 +587,11 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
if (dfRobotGravitySensor.hasSensor()) {
result = dfRobotGravitySensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
if (sht31Sensor.hasSensor()) {
result = sht31Sensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)

View File

@ -62,7 +62,7 @@ int32_t HealthTelemetryModule::runOnce()
if (max30102Sensor.hasSensor())
result = max30102Sensor.runOnce();
}
return result;
return result == UINT32_MAX ? disable() : setStartDelay();
} else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.health_measurement_enabled) {

View File

@ -65,7 +65,7 @@ int32_t PowerTelemetryModule::runOnce()
if (max17048Sensor.hasSensor() && !max17048Sensor.isInitialized())
result = max17048Sensor.runOnce();
}
return result;
return result == UINT32_MAX ? disable() : setStartDelay();
#else
return disable();
#endif
@ -100,7 +100,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
if (lastMeasurementPacket == nullptr) {
// In case of no valid packet, display "Power Telemetry", "No measurement"
display->drawString(x, y, "Power Telemetry");
@ -121,23 +121,23 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
}
// Display "Pow. From: ..."
display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) +
"V " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
"Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) +
"V " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
"Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) +
"V " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
"Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " +
String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
}
}

View File

@ -0,0 +1,44 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "DFRobotGravitySensor.h"
#include "TelemetrySensor.h"
#include <DFRobot_RainfallSensor.h>
#include <string>
DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_RAIN, "DFROBOT_RAIN") {}
int32_t DFRobotGravitySensor::runOnce()
{
LOG_INFO("Init sensor: %s", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
gravity = DFRobot_RainfallSensor_I2C(nodeTelemetrySensorsMap[sensorType].second);
status = gravity.begin();
return initI2CSensor();
}
void DFRobotGravitySensor::setup()
{
LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity.vid, gravity.pid, gravity.getFirmwareVersion().c_str());
}
bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement)
{
measurement->variant.environment_metrics.has_rainfall_1h = true;
measurement->variant.environment_metrics.has_rainfall_24h = true;
measurement->variant.environment_metrics.rainfall_1h = gravity.getRainfall(1);
measurement->variant.environment_metrics.rainfall_24h = gravity.getRainfall(24);
LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h);
LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h);
return true;
}
#endif

View File

@ -0,0 +1,29 @@
#pragma once
#ifndef _MT_DFROBOTGRAVITYSENSOR_H
#define _MT_DFROBOTGRAVITYSENSOR_H
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <DFRobot_RainfallSensor.h>
#include <string>
class DFRobotGravitySensor : public TelemetrySensor
{
private:
DFRobot_RainfallSensor_I2C gravity = DFRobot_RainfallSensor_I2C(nodeTelemetrySensorsMap[sensorType].second);
protected:
virtual void setup() override;
public:
DFRobotGravitySensor();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
};
#endif
#endif

View File

@ -1,9 +1,10 @@
#ifdef HAS_RAKPROT
#include "../variants/rak2560/RAK9154Sensor.h"
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "../modules/Telemetry/Sensor/TelemetrySensor.h"
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "RAK9154Sensor.h"
#include "TelemetrySensor.h"
#include "concurrency/Periodic.h"
#include <RAK-OneWireSerial.h>
@ -25,6 +26,8 @@ static uint16_t dc_vol = 0;
static uint8_t dc_prec = 0;
static uint8_t provision = 0;
extern RAK9154Sensor rak9154Sensor;
static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len)
{
switch (eid) {
@ -78,6 +81,7 @@ static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT
default:
break;
}
rak9154Sensor.setLastRead(millis());
break;
case SNHUBAPI_EVT_REPORT:
@ -106,6 +110,7 @@ static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT
default:
break;
}
rak9154Sensor.setLastRead(millis());
break;
@ -145,15 +150,18 @@ static int32_t onewireHandle()
int32_t RAK9154Sensor::runOnce()
{
onewirePeriodic = new Periodic("onewireHandle", onewireHandle);
if (!rak9154Sensor.isInitialized()) {
onewirePeriodic = new Periodic("onewireHandle", onewireHandle);
mySerial.begin(9600);
mySerial.begin(9600);
RakSNHub_Protocl_API.init(onewire_evt);
RakSNHub_Protocl_API.init(onewire_evt);
status = true;
initialized = true;
return 0;
status = true;
initialized = true;
}
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
void RAK9154Sensor::setup()
@ -163,7 +171,16 @@ void RAK9154Sensor::setup()
bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
return true;
if (getBusVoltageMv() > 0) {
measurement->variant.environment_metrics.has_voltage = true;
measurement->variant.environment_metrics.has_current = true;
measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000;
measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000;
return true;
} else {
return false;
}
}
uint16_t RAK9154Sensor::getBusVoltageMv()
@ -171,6 +188,11 @@ uint16_t RAK9154Sensor::getBusVoltageMv()
return dc_vol;
}
int16_t RAK9154Sensor::getCurrentMa()
{
return dc_cur;
}
int RAK9154Sensor::getBusBatteryPercent()
{
return (int)dc_prec;
@ -180,4 +202,8 @@ bool RAK9154Sensor::isCharging()
{
return (dc_cur > 0) ? true : false;
}
void RAK9154Sensor::setLastRead(uint32_t lastRead)
{
this->lastRead = lastRead;
}
#endif // HAS_RAKPROT

View File

@ -1,23 +1,30 @@
#ifdef HAS_RAKPROT
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
#ifndef _RAK9154SENSOR_H
#define _RAK9154SENSOR_H 1
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "../modules/Telemetry/Sensor/TelemetrySensor.h"
#include "../modules/Telemetry/Sensor/VoltageSensor.h"
#include "CurrentSensor.h"
#include "TelemetrySensor.h"
#include "VoltageSensor.h"
class RAK9154Sensor : public TelemetrySensor, VoltageSensor
class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
{
private:
protected:
virtual void setup() override;
uint32_t lastRead = 0;
public:
RAK9154Sensor();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual uint16_t getBusVoltageMv() override;
virtual int16_t getCurrentMa() override;
int getBusBatteryPercent();
bool isCharging();
void setLastRead(uint32_t lastRead);
};
#endif // _RAK9154SENSOR_H
#endif // HAS_RAKPROT

View File

@ -48,6 +48,13 @@ class AccelerometerThread : public concurrency::OSThread
setIntervalFromNow(0);
};
void calibrate(uint16_t forSeconds)
{
if (sensor) {
sensor->calibrate(forSeconds);
}
}
protected:
int32_t runOnce() override
{

View File

@ -4,7 +4,7 @@
BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
#ifdef RAK_4631
#if defined(RAK_4631) && !defined(RAK2560)
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
// screen is defined in main.cpp
@ -25,19 +25,21 @@ bool BMX160Sensor::init()
int32_t BMX160Sensor::runOnce()
{
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
sBmx160SensorData_t magAccel;
sBmx160SensorData_t gAccel;
/* Get a new sensor event */
sensor.getAllData(&magAccel, NULL, &gAccel);
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
// experimental calibrate routine. Limited to between 10 and 30 seconds after boot
if (millis() > 12 * 1000 && millis() < 30 * 1000) {
if (doCalibration) {
if (!showingScreen) {
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
showingScreen = true;
screen->startAlert((FrameCallback)drawFrameCalibration);
}
if (magAccel.x > highestX)
highestX = magAccel.x;
if (magAccel.x < lowestX)
@ -50,9 +52,17 @@ int32_t BMX160Sensor::runOnce()
highestZ = magAccel.z;
if (magAccel.z < lowestZ)
lowestZ = magAccel.z;
} else if (showingScreen && millis() >= 30 * 1000) {
showingScreen = false;
screen->endAlert();
uint32_t now = millis();
if (now > endCalibrationAt) {
doCalibration = false;
endCalibrationAt = 0;
showingScreen = false;
screen->endAlert();
}
// LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
// lowestY, highestY, lowestZ, highestZ);
}
int highestRealX = highestX - (highestX + lowestX) / 2;
@ -93,12 +103,25 @@ int32_t BMX160Sensor::runOnce()
heading += 270;
break;
}
screen->setHeading(heading);
#endif
return MOTION_SENSOR_CHECK_INTERVAL_MS;
}
void BMX160Sensor::calibrate(uint16_t forSeconds)
{
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
doCalibration = true;
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
endCalibrationAt = millis() + calibrateFor;
screen->setEndCalibration(endCalibrationAt);
#endif
}
#endif
#endif

View File

@ -7,7 +7,7 @@
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
#ifdef RAK_4631
#if defined(RAK_4631) && !defined(RAK2560)
#include "Fusion/Fusion.h"
#include <Rak_BMX160.h>
@ -23,6 +23,7 @@ class BMX160Sensor : public MotionSensor
explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice);
virtual bool init() override;
virtual int32_t runOnce() override;
virtual void calibrate(uint16_t forSeconds) override;
};
#else

View File

@ -2,6 +2,8 @@
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
char timeRemainingBuffer[12];
// screen is defined in main.cpp
extern graphics::Screen *screen;
@ -37,6 +39,12 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_MEDIUM);
display->drawString(x, y, "Calibrating\nCompass");
uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000;
sprintf(timeRemainingBuffer, "( %02d )", timeRemaining);
display->setFont(FONT_SMALL);
display->drawString(x, y + 40, timeRemainingBuffer);
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight());

View File

@ -40,6 +40,8 @@ class MotionSensor
// Refer to /src/concurrency/OSThread.h for more information
inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; };
virtual void calibrate(uint16_t forSeconds){};
protected:
// Turn on the screen when a tap or motion is detected
virtual void wakeScreen();
@ -53,6 +55,10 @@ class MotionSensor
#endif
ScanI2C::FoundDevice device;
// Do calibration if true
bool doCalibration = false;
uint32_t endCalibrationAt = 0;
};
namespace MotionSensorI2C

View File

@ -217,6 +217,7 @@ bool isPrivateIpAddress(const IPAddress &ip)
{.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16
{.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8
{.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32
{.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10
};
const uint32_t addr = ntohl(ip);
for (const auto &cidrRange : privateCidrRanges) {

View File

@ -1,6 +1,7 @@
#include "configuration.h"
#include <Adafruit_TinyUSB.h>
#include <Adafruit_nRFCrypto.h>
#include <InternalFileSystem.h>
#include <SPI.h>
#include <Wire.h>
#include <assert.h>
@ -130,6 +131,54 @@ int printf(const char *fmt, ...)
return res;
}
namespace
{
constexpr uint8_t NRF52_MAGIC_LFS_IS_CORRUPT = 0xF5;
constexpr uint32_t MULTIPLE_CORRUPTION_DELAY_MILLIS = 20 * 60 * 1000;
static unsigned long millis_until_formatting_again = 0;
// Report the critical error from loop(), giving a chance for the screen to be initialized first.
inline void reportLittleFSCorruptionOnce()
{
static bool report_corruption = !!millis_until_formatting_again;
if (report_corruption) {
report_corruption = false;
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE);
}
}
} // namespace
void preFSBegin()
{
// The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET
// is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS.
if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT))
return;
NRF_POWER->GPREGRET = 0;
millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS;
InternalFS.format();
LOG_INFO("LittleFS format complete; restoring default settings");
}
extern "C" void lfs_assert(const char *reason)
{
LOG_ERROR("LittleFS corruption detected: %s", reason);
if (millis_until_formatting_again > millis()) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE);
const long millis_remain = millis_until_formatting_again - millis();
LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000);
delay(millis_remain);
}
LOG_INFO("Rebooting to format LittleFS");
delay(500); // Give the serial port a bit of time to output that last message.
// Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set
// NRF_POWER->GPREGRET directly.
if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) {
NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT;
}
NVIC_SystemReset();
}
void checkSDEvents()
{
if (useSoftDevice) {
@ -154,6 +203,7 @@ void checkSDEvents()
void nrf52Loop()
{
checkSDEvents();
reportLittleFSCorruptionOnce();
}
#ifdef USE_SEMIHOSTING
@ -309,4 +359,4 @@ void enterDfuMode()
#else
enterUf2Dfu();
#endif
}
}

View File

@ -21,6 +21,10 @@
#include <sys/ioctl.h>
#include <unistd.h>
#ifdef PORTDUINO_LINUX_HARDWARE
#include <cxxabi.h>
#endif
#include "platform/portduino/USBHal.h"
std::map<configNames, int> settingsMap;
@ -196,15 +200,12 @@ void portduinoSetup()
// if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address
uint8_t dmac[6] = {0};
if (settingsStrings[spidev] == "ch341") {
ch341Hal = new Ch341Hal(0);
if (settingsStrings[lora_usb_serial_num] != "") {
ch341Hal->serial = settingsStrings[lora_usb_serial_num];
}
ch341Hal->vid = settingsMap[lora_usb_vid];
ch341Hal->pid = settingsMap[lora_usb_pid];
ch341Hal->init();
if (!ch341Hal->isInit()) {
std::cout << "Could not initialize CH341 device!" << std::endl;
try {
ch341Hal =
new Ch341Hal(0, settingsStrings[lora_usb_serial_num], settingsMap[lora_usb_vid], settingsMap[lora_usb_pid]);
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
std::cerr << "Could not initialize CH341 device!" << std::endl;
exit(EXIT_FAILURE);
}
char serial[9] = {0};
@ -318,8 +319,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line)
gpioBind(csPin);
return ERRNO_OK;
} catch (...) {
std::exception_ptr p = std::current_exception();
std::cout << "Warning, cannot claim pin " << gpio_name << (p ? p.__cxa_exception_type()->name() : "null") << std::endl;
const std::type_info *t = abi::__cxa_current_exception_type();
std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl;
return ERRNO_DISABLED;
}
#else
@ -368,6 +369,12 @@ bool loadConfig(const char *configPath)
}
}
settingsMap[sx126x_max_power] = yamlConfig["Lora"]["SX126X_MAX_POWER"].as<int>(22);
settingsMap[sx128x_max_power] = yamlConfig["Lora"]["SX128X_MAX_POWER"].as<int>(13);
settingsMap[lr1110_max_power] = yamlConfig["Lora"]["LR1110_MAX_POWER"].as<int>(22);
settingsMap[lr1120_max_power] = yamlConfig["Lora"]["LR1120_MAX_POWER"].as<int>(13);
settingsMap[rf95_max_power] = yamlConfig["Lora"]["RF95_MAX_POWER"].as<int>(20);
settingsMap[dio2_as_rf_switch] = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as<bool>(false);
settingsMap[dio3_tcxo_voltage] = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as<float>(0) * 1000;
if (settingsMap[dio3_tcxo_voltage] == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as<bool>(false)) {
@ -517,7 +524,12 @@ bool loadConfig(const char *configPath)
if (yamlConfig["Webserver"]) {
settingsMap[webserverport] = (yamlConfig["Webserver"]["Port"]).as<int>(-1);
settingsStrings[webserverrootpath] = (yamlConfig["Webserver"]["RootPath"]).as<std::string>("");
settingsStrings[webserverrootpath] =
(yamlConfig["Webserver"]["RootPath"]).as<std::string>("/usr/share/meshtasticd/web");
settingsStrings[websslkeypath] =
(yamlConfig["Webserver"]["SSLKey"]).as<std::string>("/etc/meshtasticd/ssl/private_key.pem");
settingsStrings[websslcertpath] =
(yamlConfig["Webserver"]["SSLCert"]).as<std::string>("/etc/meshtasticd/ssl/certificate.pem");
}
if (yamlConfig["General"]) {

View File

@ -27,6 +27,11 @@ enum configNames {
sx126x_ant_sw_pin,
sx126x_ant_sw_line,
sx126x_ant_sw_gpiochip,
sx126x_max_power,
sx128x_max_power,
lr1110_max_power,
lr1120_max_power,
rf95_max_power,
dio2_as_rf_switch,
dio3_tcxo_voltage,
use_rf95,
@ -76,6 +81,8 @@ enum configNames {
webserver,
webserverport,
webserverrootpath,
websslkeypath,
websslcertpath,
maxtophone,
maxnodes,
ascii_logs,
@ -94,4 +101,4 @@ int initGPIOPin(int pinNum, std::string gpioChipname, int line);
bool loadConfig(const char *configPath);
static bool ends_with(std::string_view str, std::string_view suffix);
void getMacAddr(uint8_t *dmac);
bool MAC_from_string(std::string mac_str, uint8_t *dmac);
bool MAC_from_string(std::string mac_str, uint8_t *dmac);

View File

@ -5,6 +5,7 @@
#include "platform/portduino/PortduinoGlue.h"
#include <RadioLib.h>
#include <csignal>
#include <iostream>
#include <libpinedio-usb.h>
#include <unistd.h>
@ -26,30 +27,42 @@ class Ch341Hal : public RadioLibHal
{
public:
// default constructor - initializes the base HAL and any needed private members
explicit Ch341Hal(uint8_t spiChannel, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0)
explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512,
uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0)
: RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING)
{
if (serial != "") {
strncpy(pinedio.serial_number, serial.c_str(), 8);
pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1);
}
// LOG_INFO("USB Serial: %s", pinedio.serial_number);
// There is no vendor with 0x0 -> so check
if (vid != 0x0) {
pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid);
pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid);
}
int32_t ret = pinedio_init(&pinedio, NULL);
if (ret != 0) {
std::string s = "Could not open SPI: ";
throw(s + std::to_string(ret));
}
pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0);
pinedio_set_pin_mode(&pinedio, 3, true);
pinedio_set_pin_mode(&pinedio, 5, true);
}
~Ch341Hal() { pinedio_deinit(&pinedio); }
void getSerialString(char *_serial, size_t len)
{
if (!pinedio_is_init) {
return;
}
len = len > 8 ? 8 : len;
strncpy(_serial, pinedio.serial_number, len);
}
void init() override
{
// now the SPI
spiBegin();
}
void term() override
{
// stop the SPI
spiEnd();
}
void init() override {}
void term() override {}
// GPIO-related methods (pinMode, digitalWrite etc.) should check
// RADIOLIB_NC as an alias for non-connected pins
@ -79,7 +92,7 @@ class Ch341Hal : public RadioLibHal
void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override
{
if ((interruptNum == RADIOLIB_NC)) {
if (interruptNum == RADIOLIB_NC) {
return;
}
// LOG_DEBUG("Attach interrupt to pin %d", interruptNum);
@ -88,23 +101,14 @@ class Ch341Hal : public RadioLibHal
void detachInterrupt(uint32_t interruptNum) override
{
if ((interruptNum == RADIOLIB_NC)) {
if (interruptNum == RADIOLIB_NC) {
return;
}
// LOG_DEBUG("Detach interrupt from pin %d", interruptNum);
pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum);
}
void delay(unsigned long ms) override
{
if (ms == 0) {
sched_yield();
return;
}
usleep(ms * 1000);
}
void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); }
void delayMicroseconds(unsigned long us) override
{
@ -133,62 +137,26 @@ class Ch341Hal : public RadioLibHal
long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override
{
fprintf(stderr, "pulseIn for pin %u is not supported!\n", pin);
std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl;
return 0;
}
void spiBegin()
{
if (!pinedio_is_init) {
if (serial != "") {
strncpy(pinedio.serial_number, serial.c_str(), 8);
pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1);
}
pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid);
pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid);
int32_t ret = pinedio_init(&pinedio, NULL);
if (ret != 0) {
fprintf(stderr, "Could not open SPI: %d\n", ret);
} else {
pinedio_is_init = true;
// LOG_INFO("USB Serial: %s", pinedio.serial_number);
pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0);
pinedio_set_pin_mode(&pinedio, 3, true);
pinedio_set_pin_mode(&pinedio, 5, true);
}
}
}
void spiBegin() {}
void spiBeginTransaction() {}
void spiTransfer(uint8_t *out, size_t len, uint8_t *in)
{
int32_t result = pinedio_transceive(&this->pinedio, out, in, len);
if (result < 0) {
fprintf(stderr, "Could not perform SPI transfer: %d\n", result);
int32_t ret = pinedio_transceive(&this->pinedio, out, in, len);
if (ret < 0) {
std::cerr << "Could not perform SPI transfer: " << ret << std::endl;
}
}
void spiEndTransaction() {}
void spiEnd()
{
if (pinedio_is_init) {
pinedio_deinit(&pinedio);
pinedio_is_init = false;
}
}
bool isInit() { return pinedio_is_init; }
std::string serial = "";
uint32_t pid = 0x5512;
uint32_t vid = 0x1A86;
void spiEnd() {}
private:
// the HAL can contain any additional private members
pinedio_inst pinedio = {0};
bool pinedio_is_init = false;
};
#endif
#endif

View File

@ -1,5 +1,4 @@
#pragma once
#include "../variants/rak2560/RAK9154Sensor.h"
#include "PowerStatus.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
@ -56,8 +55,8 @@ extern INA3221Sensor ina3221Sensor;
extern MAX17048Sensor max17048Sensor;
#endif
#if HAS_RAKPROT && !defined(ARCH_PORTDUINO)
#include "../variants/rak2560/RAK9154Sensor.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT && !defined(ARCH_PORTDUINO)
#include "modules/Telemetry/Sensor/RAK9154Sensor.h"
extern RAK9154Sensor rak9154Sensor;
#endif

View File

@ -29,9 +29,13 @@
// "USERPREFS_FIXED_GPS_LON": "2.294508368",
// "USERPREFS_LORACONFIG_CHANNEL_NUM": "31",
// "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST",
// "USERPREFS_SPLASH_TITLE": "DEFCONtastic",
"USERPREFS_TZ_STRING": "tzplaceholder "
// "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }",
// "USERPREFS_USE_ADMIN_KEY_1": "{}",
// "USERPREFS_USE_ADMIN_KEY_2": "{}"
}
// "USERPREFS_USE_ADMIN_KEY_2": "{}",
// "USERPREFS_OEM_TEXT": "Caterham Car Club",
// "USERPREFS_OEM_FONT_SIZE": "0",
// "USERPREFS_OEM_IMAGE_WIDTH": "50",
// "USERPREFS_OEM_IMAGE_HEIGHT": "28",
// "USERPREFS_OEM_IMAGE_DATA": "{ 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x67, 0x00, 0x00, 0x00, 0x18, 0x1F, 0xF0, 0x67, 0x00, 0x00, 0x00, 0x30, 0x1F, 0xF8, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x00, 0x60, 0x00, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7E, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x81, 0x1F, 0x06, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x0F, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x8F, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xC7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00}"
}

View File

@ -123,7 +123,7 @@ static const uint8_t A0 = PIN_A0;
*/
#define HAS_GPS 1
#define GPS_UBLOX
#define GPS_BAUDRATE 38400
#define GPS_BAUDRATE 9600
// #define PIN_GPS_WAKE (GPIO_PORT1 + 2) // An output to wake GPS, low means allow sleep, high means force wake
// Seems to be missing on this new board

View File

@ -183,8 +183,7 @@ settings.
*/
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL
extern float tcxoVoltage; // make this available everywhere
#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL
#ifdef __cplusplus
}

View File

@ -14,7 +14,9 @@ lib_deps =
${esp32_base.lib_deps}
zinggjm/GxEPD2@^1.5.1
adafruit/Adafruit NeoPixel @ ^1.12.0
build_unflags = -DARDUINO_USB_MODE=1
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1
build_flags =
;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_eink
${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_eink

View File

@ -13,7 +13,9 @@ platform_packages =
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit NeoPixel @ ^1.12.0
build_unflags = -DARDUINO_USB_MODE=1
build_unflags =
${esp32s3_base.build_unflags}
-DARDUINO_USB_MODE=1
build_flags =
;${esp32_base.build_flags} -D MY_ESP32S3_DIY -I variants/my_esp32s3_diy_oled
${esp32_base.build_flags} -D PRIVATE_HW -I variants/my_esp32s3_diy_oled

View File

@ -1,113 +0,0 @@
import struct
Import("env") # noqa: F821
# Parse input and create UF2 file
def create_uf2(source, target, env):
# source_hex = target[0].get_abspath()
source_hex = target[0].get_string(False)
source_hex = ".\\" + source_hex
print("#########################################################")
print("Create UF2 from " + source_hex)
print("#########################################################")
# print("Source: " + source_hex)
target = source_hex.replace(".hex", "")
target = target + ".uf2"
# print("Target: " + target)
with open(source_hex, mode="rb") as f:
inpbuf = f.read()
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
write_file(target, outbuf)
print("#########################################################")
print(target + " is ready to flash to target device")
print("#########################################################")
# Add callback after .hex file was created
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", create_uf2) # noqa: F821
# UF2 creation taken from uf2conv.py
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
familyid = 0xADA52840
class Block:
def __init__(self, addr):
self.addr = addr
self.bytes = bytearray(256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(
"<IIIIIIII",
UF2_MAGIC_START0,
UF2_MAGIC_START1,
flags,
self.addr,
256,
blockno,
numblocks,
familyid,
)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
# print("Wrote %d bytes to %s." % (len(buf), name))
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split("\n"):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i : i + 2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
assert (upper & 0xFFFF) == 0
elif tp == 1:
break
elif tp == 0:
addr = upper | (rec[1] << 8) | rec[2]
if appstartaddr is None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xFF != addr & ~0xFF:
currblock = Block(addr & ~0xFF)
blocks.append(currblock)
currblock.bytes[addr & 0xFF] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile

View File

@ -15,8 +15,6 @@ lib_deps =
${nrf52840_base.lib_deps}
${networking_base.lib_deps}
melopero/Melopero RV3028@^1.1.0
rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
beegee-tokyo/RAKwireless RAK12034@^1.0.0
https://github.com/beegee-tokyo/RAK-OneWireSerial.git#0.0.2
debug_tool = jlink
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)

View File

@ -70,5 +70,8 @@
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define TCXO_OPTIONAL // handle Indicator V1 and V2
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define USE_VIRTUAL_KEYBOARD 1
#define DISPLAY_CLOCK_FRAME 1

View File

@ -1,4 +1,4 @@
[VERSION]
major = 2
minor = 5
build = 19
build = 21