mirror of https://github.com/meshcore-dev/MeshCore
11 changed files with 1334 additions and 0 deletions
@ -0,0 +1,184 @@ |
|||
// Ethernet support for Broken Circuit Ranch POE Ethernet
|
|||
// http://www.brokencircuitranch.com
|
|||
// Kent Andersen
|
|||
|
|||
#ifdef HAS_ETHERNET |
|||
|
|||
#include "SerialEthernetInterface.h" |
|||
#include <Ethernet3.h> |
|||
#include <SPI.h> |
|||
|
|||
bool SerialEthernetInterface::begin(SPIClass& spi, int port, int cs_pin, int rst_pin, uint8_t mac[6]) { |
|||
|
|||
// Set CS pin before anything else
|
|||
Ethernet.setCsPin(cs_pin); |
|||
|
|||
// Reset W5500 if reset pin provided
|
|||
if (rst_pin >= 0) { |
|||
Ethernet.setRstPin(rst_pin); |
|||
Ethernet.hardreset(); |
|||
delay(200); |
|||
} |
|||
|
|||
// Try DHCP
|
|||
ETH_DEBUG_PRINTLN("Starting DHCP..."); |
|||
int result = Ethernet.begin(mac); |
|||
if (result == 0) { |
|||
ETH_DEBUG_PRINTLN("DHCP failed, using static IP 192.168.1.200"); |
|||
IPAddress staticIP(192, 168, 1, 200); |
|||
IPAddress subnet(255, 255, 255, 0); |
|||
IPAddress gateway(192, 168, 1, 1); |
|||
IPAddress dns(8, 8, 8, 8); |
|||
Ethernet.begin(mac, staticIP, subnet, gateway, dns); |
|||
} |
|||
|
|||
ETH_DEBUG_PRINTLN("Ethernet IP: %s", Ethernet.localIP().toString().c_str()); |
|||
|
|||
// Start TCP server
|
|||
server = new ConcreteEthernetServer(port); |
|||
server->begin(port); |
|||
_ethInitialized = true; |
|||
|
|||
ETH_DEBUG_PRINTLN("TCP server started on port %d", port); |
|||
return true; |
|||
} |
|||
|
|||
void SerialEthernetInterface::enable() { |
|||
if (_isEnabled) return; |
|||
_isEnabled = true; |
|||
clearBuffers(); |
|||
} |
|||
|
|||
void SerialEthernetInterface::disable() { |
|||
_isEnabled = false; |
|||
} |
|||
|
|||
size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { |
|||
if (len > MAX_FRAME_SIZE) { |
|||
ETH_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len); |
|||
return 0; |
|||
} |
|||
|
|||
if (deviceConnected && len > 0) { |
|||
if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) { |
|||
ETH_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); |
|||
return 0; |
|||
} |
|||
send_queue[send_queue_len].len = len; |
|||
memcpy(send_queue[send_queue_len].buf, src, len); |
|||
send_queue_len++; |
|||
return len; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
bool SerialEthernetInterface::isWriteBusy() const { |
|||
return false; |
|||
} |
|||
|
|||
bool SerialEthernetInterface::hasReceivedFrameHeader() { |
|||
return received_frame_header.type != 0 && received_frame_header.length != 0; |
|||
} |
|||
|
|||
void SerialEthernetInterface::resetReceivedFrameHeader() { |
|||
received_frame_header.type = 0; |
|||
received_frame_header.length = 0; |
|||
} |
|||
|
|||
size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { |
|||
|
|||
if (!_ethInitialized || !server) return 0; |
|||
|
|||
// Maintain DHCP lease
|
|||
Ethernet.maintain(); |
|||
|
|||
// Check for new client
|
|||
EthernetClient newClient = server->available(); |
|||
if (newClient) { |
|||
deviceConnected = false; |
|||
client.stop(); |
|||
client = newClient; |
|||
resetReceivedFrameHeader(); |
|||
} |
|||
|
|||
if (client.connected()) { |
|||
if (!deviceConnected) { |
|||
ETH_DEBUG_PRINTLN("Client connected"); |
|||
deviceConnected = true; |
|||
} |
|||
} else { |
|||
if (deviceConnected) { |
|||
deviceConnected = false; |
|||
ETH_DEBUG_PRINTLN("Client disconnected"); |
|||
} |
|||
} |
|||
|
|||
if (deviceConnected) { |
|||
if (send_queue_len > 0) { |
|||
_last_write = millis(); |
|||
int len = send_queue[0].len; |
|||
|
|||
uint8_t pkt[3 + len]; |
|||
pkt[0] = '>'; |
|||
pkt[1] = (len & 0xFF); |
|||
pkt[2] = (len >> 8); |
|||
memcpy(&pkt[3], send_queue[0].buf, len); |
|||
client.write(pkt, 3 + len); |
|||
|
|||
send_queue_len--; |
|||
for (int i = 0; i < send_queue_len; i++) { |
|||
send_queue[i] = send_queue[i + 1]; |
|||
} |
|||
} else { |
|||
if (!hasReceivedFrameHeader()) { |
|||
if (client.available() >= 3) { |
|||
client.readBytes(&received_frame_header.type, 1); |
|||
client.readBytes((uint8_t*)&received_frame_header.length, 2); |
|||
} |
|||
} |
|||
|
|||
if (hasReceivedFrameHeader()) { |
|||
int available = client.available(); |
|||
int frame_type = received_frame_header.type; |
|||
int frame_length = received_frame_header.length; |
|||
|
|||
if (frame_length > available) { |
|||
ETH_DEBUG_PRINTLN("Waiting for %d more bytes", frame_length - available); |
|||
return 0; |
|||
} |
|||
|
|||
if (frame_length > MAX_FRAME_SIZE) { |
|||
ETH_DEBUG_PRINTLN("Skipping oversized frame: %d bytes", frame_length); |
|||
while (frame_length > 0) { |
|||
uint8_t skip[1]; |
|||
frame_length -= client.read(skip, 1); |
|||
} |
|||
resetReceivedFrameHeader(); |
|||
return 0; |
|||
} |
|||
|
|||
if (frame_type != '<') { |
|||
ETH_DEBUG_PRINTLN("Skipping unexpected frame type: 0x%x", frame_type); |
|||
while (frame_length > 0) { |
|||
uint8_t skip[1]; |
|||
frame_length -= client.read(skip, 1); |
|||
} |
|||
resetReceivedFrameHeader(); |
|||
return 0; |
|||
} |
|||
|
|||
client.readBytes(dest, frame_length); |
|||
resetReceivedFrameHeader(); |
|||
return frame_length; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
bool SerialEthernetInterface::isConnected() const { |
|||
return deviceConnected; |
|||
} |
|||
|
|||
#endif |
|||
@ -0,0 +1,92 @@ |
|||
#pragma once |
|||
// Ethernet support for Broken Circuit Ranch POE Ethernet
|
|||
// http://www.brokencircuitranch.com
|
|||
// Kent Andersen
|
|||
|
|||
#ifdef HAS_ETHERNET |
|||
|
|||
#include "../BaseSerialInterface.h" |
|||
#include <Ethernet3.h> |
|||
#include <SPI.h> |
|||
|
|||
// Workaround: ESP32 Arduino core Server.h declares begin(uint16_t) as pure virtual
|
|||
// but Ethernet3 only implements begin() with no args.
|
|||
// This subclass satisfies the compiler by implementing the missing override.
|
|||
class ConcreteEthernetServer : public EthernetServer { |
|||
public: |
|||
ConcreteEthernetServer(uint16_t port) : EthernetServer(port) {} |
|||
void begin(uint16_t port) override { EthernetServer::begin(); } |
|||
}; |
|||
|
|||
class SerialEthernetInterface : public BaseSerialInterface { |
|||
bool deviceConnected; |
|||
bool _isEnabled; |
|||
bool _ethInitialized; |
|||
unsigned long _last_write; |
|||
|
|||
ConcreteEthernetServer* server; |
|||
EthernetClient client; |
|||
|
|||
struct FrameHeader { |
|||
uint8_t type; |
|||
uint16_t length; |
|||
}; |
|||
|
|||
struct Frame { |
|||
uint8_t len; |
|||
uint8_t buf[MAX_FRAME_SIZE]; |
|||
}; |
|||
|
|||
FrameHeader received_frame_header; |
|||
|
|||
#define ETH_FRAME_QUEUE_SIZE 4 |
|||
int recv_queue_len; |
|||
Frame recv_queue[ETH_FRAME_QUEUE_SIZE]; |
|||
int send_queue_len; |
|||
Frame send_queue[ETH_FRAME_QUEUE_SIZE]; |
|||
|
|||
void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } |
|||
|
|||
public: |
|||
SerialEthernetInterface() : server(nullptr), client(EthernetClient()) { |
|||
deviceConnected = false; |
|||
_isEnabled = false; |
|||
_ethInitialized = false; |
|||
_last_write = 0; |
|||
send_queue_len = recv_queue_len = 0; |
|||
received_frame_header.type = 0; |
|||
received_frame_header.length = 0; |
|||
} |
|||
|
|||
// spi: shared SPI bus (vspi from target.cpp)
|
|||
// port: TCP port to listen on
|
|||
// cs_pin: W5500 chip select pin
|
|||
// rst_pin: W5500 reset pin (-1 if not used)
|
|||
// mac: 6-byte MAC address
|
|||
bool begin(SPIClass& spi, int port, int cs_pin, int rst_pin, uint8_t mac[6]); |
|||
|
|||
void enable() override; |
|||
void disable() override; |
|||
bool isEnabled() const override { return _isEnabled; } |
|||
|
|||
bool isConnected() const override; |
|||
bool isWriteBusy() const override; |
|||
|
|||
size_t writeFrame(const uint8_t src[], size_t len) override; |
|||
size_t checkRecvFrame(uint8_t dest[]) override; |
|||
|
|||
bool hasReceivedFrameHeader(); |
|||
void resetReceivedFrameHeader(); |
|||
bool isEthernetInitialized() const { return _ethInitialized; } |
|||
}; |
|||
|
|||
#if ETH_DEBUG_LOGGING && ARDUINO |
|||
#include <Arduino.h> |
|||
#define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) |
|||
#define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) |
|||
#else |
|||
#define ETH_DEBUG_PRINT(...) {} |
|||
#define ETH_DEBUG_PRINTLN(...) {} |
|||
#endif |
|||
|
|||
#endif |
|||
@ -0,0 +1,11 @@ |
|||
@echo off |
|||
setlocal |
|||
|
|||
cd /d "%~dp0\.." |
|||
|
|||
python -m pip install --upgrade pip |
|||
python -m pip install pyinstaller pyserial esptool |
|||
python -m PyInstaller --onefile --console --name meshcore-configurator tools\meshcore_configurator.py |
|||
|
|||
echo. |
|||
echo Built: dist\meshcore-configurator.exe |
|||
@ -0,0 +1,536 @@ |
|||
#!/usr/bin/env python3 |
|||
""" |
|||
USB serial configurator for MeshCore repeater and room-server firmware. |
|||
|
|||
This avoids the browser Web Serial configurator and talks directly to the |
|||
firmware CLI at 115200 baud. |
|||
|
|||
Examples: |
|||
python3 tools/meshcore_configurator.py |
|||
python3 tools/meshcore_configurator.py --port /dev/ttyUSB0 --command "get radio" |
|||
python3 tools/meshcore_configurator.py --port /dev/ttyUSB0 --us-preset --reboot |
|||
""" |
|||
|
|||
from __future__ import annotations |
|||
|
|||
import argparse |
|||
import os |
|||
import sys |
|||
import time |
|||
from dataclasses import dataclass |
|||
from pathlib import Path |
|||
from typing import Callable, Iterable |
|||
|
|||
try: |
|||
import serial |
|||
from serial.tools import list_ports |
|||
except ImportError: |
|||
serial = None |
|||
list_ports = None |
|||
|
|||
try: |
|||
import esptool |
|||
except ImportError: |
|||
esptool = None |
|||
|
|||
|
|||
DEFAULT_BAUD = 115200 |
|||
DEFAULT_TIMEOUT = 1.2 |
|||
DEFAULT_FLASH_BAUD = 460800 |
|||
DEFAULT_FLASH_ADDRESS = "0x0" |
|||
US_CANADA_RECOMMENDED = "910.525,62.5,7,5" |
|||
|
|||
|
|||
@dataclass(frozen=True) |
|||
class Setting: |
|||
key: str |
|||
label: str |
|||
kind: str = "text" |
|||
choices: tuple[str, ...] = () |
|||
help: str = "" |
|||
reboot: bool = False |
|||
|
|||
@property |
|||
def get_command(self) -> str: |
|||
return f"get {self.key}" |
|||
|
|||
def set_command(self, value: str) -> str: |
|||
return f"set {self.key} {value}" |
|||
|
|||
|
|||
SETTINGS: tuple[Setting, ...] = ( |
|||
Setting("radio", "Radio params: freq,bw,sf,cr", help="Example: 910.525,62.5,7,5", reboot=True), |
|||
Setting("freq", "Frequency MHz", help="Example: 910.525", reboot=True), |
|||
Setting("tx", "LoRa chip TX power dBm", kind="int", help="Hammer default build flag is 22 dBm"), |
|||
Setting("name", "Node name"), |
|||
Setting("lat", "Latitude", kind="float"), |
|||
Setting("lon", "Longitude", kind="float"), |
|||
Setting("guest.password", "Room guest password"), |
|||
Setting("owner.info", "Owner info", help="Use | where you want a newline"), |
|||
Setting("repeat", "Repeat packets", kind="choice", choices=("on", "off")), |
|||
Setting("af", "Airtime factor", kind="float", help="Higher means longer silent period after TX"), |
|||
Setting("txdelay", "Flood retransmit delay factor", kind="float"), |
|||
Setting("direct.txdelay", "Direct retransmit delay factor", kind="float"), |
|||
Setting("rxdelay", "Receive processing delay", kind="float"), |
|||
Setting("flood.max", "Max flood hops", kind="int", help="0-64"), |
|||
Setting("path.hash.mode", "Path hash mode", kind="choice", choices=("0", "1", "2")), |
|||
Setting("loop.detect", "Loop detection", kind="choice", choices=("off", "minimal", "moderate", "strict")), |
|||
Setting("multi.acks", "Multi-acks", kind="choice", choices=("0", "1")), |
|||
Setting("flood.advert.interval", "Flood advert interval hours", kind="int", help="0 disables, otherwise 3-168"), |
|||
Setting("advert.interval", "Zero-hop advert interval minutes", kind="int", help="0 disables, otherwise 60-240"), |
|||
Setting("int.thresh", "Interference threshold", kind="int"), |
|||
Setting("agc.reset.interval", "AGC reset interval seconds", kind="int", help="Rounded down to multiple of 4"), |
|||
Setting("allow.read.only", "Room allow read-only", kind="choice", choices=("on", "off")), |
|||
Setting("radio.rxgain", "SX126x boosted RX gain", kind="choice", choices=("on", "off")), |
|||
Setting("adc.multiplier", "Battery ADC multiplier", kind="float", help="May be unsupported on Hammer"), |
|||
Setting("bridge.enabled", "Bridge enabled", kind="choice", choices=("on", "off")), |
|||
Setting("bridge.delay", "Bridge delay ms", kind="int"), |
|||
Setting("bridge.source", "Bridge source", kind="choice", choices=("rx", "tx")), |
|||
Setting("bridge.channel", "ESPNow bridge channel", kind="int", help="1-14"), |
|||
Setting("bridge.secret", "ESPNow bridge secret"), |
|||
) |
|||
|
|||
INFO_COMMANDS: tuple[tuple[str, str], ...] = ( |
|||
("ver", "Firmware version"), |
|||
("board", "Board name"), |
|||
("get role", "Firmware role"), |
|||
("get public.key", "Public key"), |
|||
("get bridge.type", "Bridge type"), |
|||
("powersaving", "Power saving"), |
|||
("clock", "Clock"), |
|||
("stats-core", "Core stats"), |
|||
("stats-radio", "Radio stats"), |
|||
("stats-packets", "Packet stats"), |
|||
("neighbors", "Neighbors"), |
|||
("get acl", "ACL"), |
|||
) |
|||
|
|||
|
|||
class MeshCoreCLI: |
|||
def __init__(self, port: str, baud: int = DEFAULT_BAUD, timeout: float = DEFAULT_TIMEOUT): |
|||
if serial is None: |
|||
raise RuntimeError("pyserial is not installed. Install it with: python3 -m pip install pyserial") |
|||
self.port = port |
|||
self.baud = baud |
|||
self.timeout = timeout |
|||
self.ser = serial.Serial(port, baudrate=baud, timeout=timeout, write_timeout=timeout) |
|||
time.sleep(0.2) |
|||
self.drain() |
|||
|
|||
def close(self) -> None: |
|||
self.ser.close() |
|||
|
|||
def drain(self) -> str: |
|||
time.sleep(0.05) |
|||
data = self.ser.read(self.ser.in_waiting or 1) |
|||
chunks = [data] if data else [] |
|||
while self.ser.in_waiting: |
|||
chunks.append(self.ser.read(self.ser.in_waiting)) |
|||
time.sleep(0.02) |
|||
return b"".join(chunks).decode(errors="replace") |
|||
|
|||
def command(self, command: str, wait: float = 0.25) -> str: |
|||
self.drain() |
|||
self.ser.write(command.encode("utf-8") + b"\r") |
|||
self.ser.flush() |
|||
time.sleep(wait) |
|||
chunks = [] |
|||
deadline = time.monotonic() + self.timeout |
|||
while time.monotonic() < deadline: |
|||
waiting = self.ser.in_waiting |
|||
if waiting: |
|||
chunks.append(self.ser.read(waiting)) |
|||
deadline = time.monotonic() + 0.15 |
|||
else: |
|||
time.sleep(0.03) |
|||
text = b"".join(chunks).decode(errors="replace") |
|||
return clean_reply(command, text) |
|||
|
|||
def get(self, key: str) -> str: |
|||
return self.command(f"get {key}") |
|||
|
|||
def set(self, key: str, value: str) -> str: |
|||
return self.command(f"set {key} {value}") |
|||
|
|||
def password(self, value: str) -> str: |
|||
return self.command(f"password {value}") |
|||
|
|||
def reboot(self) -> str: |
|||
return self.command("reboot", wait=0.1) |
|||
|
|||
def apply_us_canada_recommended(self) -> str: |
|||
return self.set("radio", US_CANADA_RECOMMENDED) |
|||
|
|||
|
|||
def clean_reply(command: str, text: str) -> str: |
|||
lines = [] |
|||
for raw in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"): |
|||
line = raw.strip() |
|||
if not line: |
|||
continue |
|||
if line == command or line.endswith(command): |
|||
continue |
|||
if line.startswith("-> "): |
|||
line = line[3:].strip() |
|||
if line.startswith(" -> "): |
|||
line = line[5:].strip() |
|||
lines.append(line) |
|||
return "\n".join(lines).strip() |
|||
|
|||
|
|||
def require_pyserial() -> None: |
|||
if serial is None: |
|||
print("Missing dependency: pyserial") |
|||
print("Install it with:") |
|||
print(" python3 -m pip install pyserial") |
|||
print() |
|||
print("On Ubuntu you may also need serial permissions:") |
|||
print(" sudo usermod -aG dialout $USER") |
|||
print("Then log out and back in.") |
|||
raise SystemExit(2) |
|||
|
|||
|
|||
def require_esptool() -> None: |
|||
if esptool is None: |
|||
print("Missing dependency: esptool") |
|||
print("Install it with:") |
|||
print(" python3 -m pip install esptool") |
|||
raise SystemExit(2) |
|||
|
|||
|
|||
def available_ports() -> list[str]: |
|||
require_pyserial() |
|||
return [p.device for p in list_ports.comports()] |
|||
|
|||
|
|||
def choose_port(explicit_port: str | None) -> str: |
|||
if explicit_port: |
|||
return explicit_port |
|||
|
|||
ports = available_ports() |
|||
if not ports: |
|||
print("No serial ports found.") |
|||
print("Plug in the Hammer and check: ls -l /dev/ttyUSB* /dev/ttyACM*") |
|||
print("If the port exists but this fails, add yourself to dialout:") |
|||
print(" sudo usermod -aG dialout $USER") |
|||
raise SystemExit(1) |
|||
if len(ports) == 1: |
|||
return ports[0] |
|||
|
|||
print("Serial ports:") |
|||
for index, port in enumerate(ports, 1): |
|||
print(f" {index}. {port}") |
|||
choice = input("Select port: ").strip() |
|||
if choice.isdigit() and 1 <= int(choice) <= len(ports): |
|||
return ports[int(choice) - 1] |
|||
return choice |
|||
|
|||
|
|||
def flash_esp32_firmware(port: str, firmware: str, baud: int = DEFAULT_FLASH_BAUD, |
|||
address: str = DEFAULT_FLASH_ADDRESS) -> None: |
|||
require_esptool() |
|||
firmware_path = Path(firmware).expanduser() |
|||
if not firmware_path.exists(): |
|||
raise FileNotFoundError(firmware) |
|||
|
|||
args = [ |
|||
"--chip", "esp32", |
|||
"--port", port, |
|||
"--baud", str(baud), |
|||
"--before", "default_reset", |
|||
"--after", "hard_reset", |
|||
"write_flash", |
|||
"-z", |
|||
address, |
|||
str(firmware_path), |
|||
] |
|||
|
|||
print("Flashing ESP32 firmware...") |
|||
print(f"Port: {port}") |
|||
print(f"File: {firmware_path}") |
|||
print(f"Address: {address}") |
|||
print() |
|||
esptool.main(args) |
|||
|
|||
|
|||
def print_table(rows: Iterable[tuple[str, str]]) -> None: |
|||
rows = list(rows) |
|||
width = max((len(left) for left, _ in rows), default=0) |
|||
for left, right in rows: |
|||
print(f"{left:<{width}} {right}") |
|||
|
|||
|
|||
def show_overview(dev: MeshCoreCLI) -> None: |
|||
rows = [] |
|||
for command, label in INFO_COMMANDS[:7]: |
|||
rows.append((label + ":", dev.command(command) or "(no reply)")) |
|||
print_table(rows) |
|||
print() |
|||
rows = [] |
|||
for setting in SETTINGS: |
|||
reply = dev.command(setting.get_command) |
|||
if reply.startswith("> "): |
|||
reply = reply[2:] |
|||
rows.append((setting.label + ":", reply or "(unsupported/no reply)")) |
|||
print_table(rows) |
|||
|
|||
|
|||
def prompt_value(setting: Setting) -> str | None: |
|||
if setting.choices: |
|||
print(f"Choices: {', '.join(setting.choices)}") |
|||
if setting.help: |
|||
print(setting.help) |
|||
value = input(f"New value for {setting.label}: ").strip() |
|||
if not value: |
|||
return None |
|||
return value |
|||
|
|||
|
|||
def configure_setting(dev: MeshCoreCLI) -> None: |
|||
for index, setting in enumerate(SETTINGS, 1): |
|||
print(f"{index:2}. {setting.label} [{setting.key}]") |
|||
choice = input("Setting number or key: ").strip() |
|||
setting = None |
|||
if choice.isdigit() and 1 <= int(choice) <= len(SETTINGS): |
|||
setting = SETTINGS[int(choice) - 1] |
|||
else: |
|||
setting = next((item for item in SETTINGS if item.key == choice), None) |
|||
if setting is None: |
|||
print("Unknown setting.") |
|||
return |
|||
|
|||
current = dev.command(setting.get_command) |
|||
print(f"Current: {current or '(no reply)'}") |
|||
value = prompt_value(setting) |
|||
if value is None: |
|||
return |
|||
if setting.choices and value not in setting.choices: |
|||
print("Not one of the listed choices.") |
|||
return |
|||
reply = dev.command(setting.set_command(value)) |
|||
print(reply or "(no reply)") |
|||
if setting.reboot: |
|||
print("This setting needs a reboot before the radio uses it.") |
|||
|
|||
|
|||
def raw_command(dev: MeshCoreCLI) -> None: |
|||
print("Raw mode. Empty line exits.") |
|||
while True: |
|||
command = input("meshcore> ").strip() |
|||
if not command: |
|||
return |
|||
print(dev.command(command) or "(no reply)") |
|||
|
|||
|
|||
def region_menu(dev: MeshCoreCLI) -> None: |
|||
while True: |
|||
print() |
|||
print("Region menu") |
|||
print(" 1. List regions") |
|||
print(" 2. Show region") |
|||
print(" 3. Set home region") |
|||
print(" 4. Allow flood for region") |
|||
print(" 5. Deny flood for region") |
|||
print(" 6. Create region") |
|||
print(" 7. Remove region") |
|||
print(" 8. Save region changes") |
|||
print(" 9. Back") |
|||
choice = input("> ").strip() |
|||
if choice == "1": |
|||
print(dev.command("region") or dev.command("region list allowed") or "(no reply)") |
|||
elif choice == "2": |
|||
name = input("Region name or *: ").strip() |
|||
if name: |
|||
print(dev.command(f"region get {name}") or "(no reply)") |
|||
elif choice == "3": |
|||
name = input("Home region name: ").strip() |
|||
print(dev.command(f"region home {name}") if name else dev.command("region home")) |
|||
elif choice == "4": |
|||
name = input("Region name or *: ").strip() |
|||
if name: |
|||
print(dev.command(f"region allowf {name}") or "(no reply)") |
|||
elif choice == "5": |
|||
name = input("Region name or *: ").strip() |
|||
if name: |
|||
print(dev.command(f"region denyf {name}") or "(no reply)") |
|||
elif choice == "6": |
|||
name = input("New region name: ").strip() |
|||
parent = input("Parent region, blank for default: ").strip() |
|||
if name: |
|||
command = f"region put {name} {parent}".strip() |
|||
print(dev.command(command) or "(no reply)") |
|||
elif choice == "7": |
|||
name = input("Region name to remove: ").strip() |
|||
if name: |
|||
print(dev.command(f"region remove {name}") or "(no reply)") |
|||
elif choice == "8": |
|||
print(dev.command("region save") or "(no reply)") |
|||
elif choice == "9": |
|||
return |
|||
|
|||
|
|||
def gps_menu(dev: MeshCoreCLI) -> None: |
|||
while True: |
|||
print() |
|||
print("GPS menu") |
|||
print(" 1. GPS status") |
|||
print(" 2. GPS on") |
|||
print(" 3. GPS off") |
|||
print(" 4. Set node lat/lon from current GPS fix") |
|||
print(" 5. Sync clock from GPS") |
|||
print(" 6. Show GPS advert policy") |
|||
print(" 7. Advert saved lat/lon") |
|||
print(" 8. Advert live GPS location") |
|||
print(" 9. Hide location in adverts") |
|||
print(" 10. Send advert") |
|||
print(" 11. Back") |
|||
choice = input("> ").strip() |
|||
if choice == "1": |
|||
print(dev.command("gps") or "(no reply)") |
|||
elif choice == "2": |
|||
print(dev.command("gps on") or "(no reply)") |
|||
elif choice == "3": |
|||
print(dev.command("gps off") or "(no reply)") |
|||
elif choice == "4": |
|||
print(dev.command("gps setloc") or "(no reply)") |
|||
elif choice == "5": |
|||
print(dev.command("gps sync") or "(no reply)") |
|||
elif choice == "6": |
|||
print(dev.command("gps advert") or "(no reply)") |
|||
elif choice == "7": |
|||
print(dev.command("gps advert prefs") or "(no reply)") |
|||
elif choice == "8": |
|||
print(dev.command("gps advert share") or "(no reply)") |
|||
elif choice == "9": |
|||
print(dev.command("gps advert none") or "(no reply)") |
|||
elif choice == "10": |
|||
print(dev.command("advert") or "(no reply)") |
|||
elif choice == "11": |
|||
return |
|||
|
|||
|
|||
def firmware_update_menu(dev: MeshCoreCLI) -> None: |
|||
print() |
|||
print("Firmware update") |
|||
print("Use a merged ESP32 image when flashing at 0x0.") |
|||
print("For Hammer builds, PlatformIO creates:") |
|||
print(" .pio/build/hammer_sx1262_repeater/firmware-merged.bin") |
|||
print() |
|||
firmware = input("Firmware .bin path: ").strip() |
|||
if not firmware: |
|||
return |
|||
firmware = os.path.expanduser(firmware) |
|||
address = input(f"Flash address [{DEFAULT_FLASH_ADDRESS}]: ").strip() or DEFAULT_FLASH_ADDRESS |
|||
baud_text = input(f"Flash baud [{DEFAULT_FLASH_BAUD}]: ").strip() |
|||
flash_baud = int(baud_text) if baud_text else DEFAULT_FLASH_BAUD |
|||
|
|||
confirm = input(f"Flash {firmware} to {dev.port} at {address}? Type YES: ").strip() |
|||
if confirm != "YES": |
|||
print("Canceled.") |
|||
return |
|||
|
|||
dev.close() |
|||
try: |
|||
flash_esp32_firmware(dev.port, firmware, flash_baud, address) |
|||
finally: |
|||
try: |
|||
dev.ser.open() |
|||
time.sleep(0.5) |
|||
dev.drain() |
|||
except Exception: |
|||
pass |
|||
|
|||
|
|||
def interactive(dev: MeshCoreCLI) -> None: |
|||
actions: tuple[tuple[str, str, Callable[[MeshCoreCLI], None]], ...] = ( |
|||
("1", "Show all known settings", show_overview), |
|||
("2", "Change a setting", configure_setting), |
|||
("3", "Apply US/Canada recommended radio preset", lambda d: print(d.apply_us_canada_recommended())), |
|||
("4", "Change admin password", lambda d: print(d.password(input("New admin password: ").strip()))), |
|||
("5", "Send advert", lambda d: print(d.command("advert"))), |
|||
("6", "Send zero-hop advert", lambda d: print(d.command("advert.zerohop"))), |
|||
("7", "Power saving on", lambda d: print(d.command("powersaving on"))), |
|||
("8", "Power saving off", lambda d: print(d.command("powersaving off"))), |
|||
("9", "GPS", gps_menu), |
|||
("10", "Region management", region_menu), |
|||
("11", "Raw command mode", raw_command), |
|||
("12", "Firmware update", firmware_update_menu), |
|||
("13", "Reboot", lambda d: print(d.reboot() or "Reboot command sent.")), |
|||
) |
|||
while True: |
|||
print() |
|||
print(f"MeshCore configurator connected to {dev.port}") |
|||
for key, label, _ in actions: |
|||
print(f" {key}. {label}") |
|||
print(" q. Quit") |
|||
choice = input("> ").strip().lower() |
|||
if choice in ("q", "quit", "exit"): |
|||
return |
|||
action = next((item for item in actions if item[0] == choice), None) |
|||
if action is None: |
|||
print("Unknown choice.") |
|||
continue |
|||
action[2](dev) |
|||
|
|||
|
|||
def parse_args() -> argparse.Namespace: |
|||
parser = argparse.ArgumentParser(description="Configure MeshCore firmware over USB serial.") |
|||
parser.add_argument("--port", help="Serial port, for example /dev/ttyUSB0") |
|||
parser.add_argument("--baud", type=int, default=DEFAULT_BAUD) |
|||
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT) |
|||
parser.add_argument("--command", action="append", help="Run one raw command and print its reply. Can be used more than once.") |
|||
parser.add_argument("--set", nargs=2, metavar=("KEY", "VALUE"), action="append", help="Set a MeshCore preference.") |
|||
parser.add_argument("--get", metavar="KEY", action="append", help="Get a MeshCore preference.") |
|||
parser.add_argument("--us-preset", action="store_true", help=f"Apply radio preset {US_CANADA_RECOMMENDED}.") |
|||
parser.add_argument("--reboot", action="store_true", help="Reboot after other commands.") |
|||
parser.add_argument("--flash", metavar="BIN", help="Flash a merged ESP32 firmware image, usually firmware-merged.bin.") |
|||
parser.add_argument("--flash-address", default=DEFAULT_FLASH_ADDRESS, help="Flash address for --flash. Use 0x0 for merged images.") |
|||
parser.add_argument("--flash-baud", type=int, default=DEFAULT_FLASH_BAUD, help="Baud rate for --flash.") |
|||
parser.add_argument("--list-ports", action="store_true", help="List serial ports and exit.") |
|||
return parser.parse_args() |
|||
|
|||
|
|||
def main() -> int: |
|||
args = parse_args() |
|||
require_pyserial() |
|||
|
|||
if args.list_ports: |
|||
ports = available_ports() |
|||
if ports: |
|||
print("\n".join(ports)) |
|||
return 0 |
|||
print("No serial ports found.") |
|||
return 1 |
|||
|
|||
port = choose_port(args.port) |
|||
if args.flash: |
|||
flash_esp32_firmware(port, args.flash, args.flash_baud, args.flash_address) |
|||
return 0 |
|||
|
|||
dev = MeshCoreCLI(port, args.baud, args.timeout) |
|||
try: |
|||
did_one_shot = False |
|||
for key in args.get or (): |
|||
did_one_shot = True |
|||
print(dev.get(key)) |
|||
for key, value in args.set or (): |
|||
did_one_shot = True |
|||
print(dev.set(key, value)) |
|||
if args.us_preset: |
|||
did_one_shot = True |
|||
print(dev.apply_us_canada_recommended()) |
|||
for command in args.command or (): |
|||
did_one_shot = True |
|||
print(dev.command(command)) |
|||
if args.reboot: |
|||
did_one_shot = True |
|||
print(dev.reboot() or "Reboot command sent.") |
|||
if not did_one_shot: |
|||
interactive(dev) |
|||
finally: |
|||
dev.close() |
|||
return 0 |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
raise SystemExit(main()) |
|||
@ -0,0 +1,185 @@ |
|||
; ============================================================ |
|||
; Broken Circuit Ranch Hammer Board Variant builds |
|||
; www.brokencircuitranch.com |
|||
; ============================================================ |
|||
|
|||
[hammer] |
|||
extends = esp32_base |
|||
board = esp32doit-devkit-v1 |
|||
build_flags = |
|||
${esp32_base.build_flags} |
|||
-I variants/hammer |
|||
-D HAMMER_BOARD |
|||
-D HAS_SCREEN |
|||
-D ENV_INCLUDE_GPS=1 |
|||
-D PIN_GPS_RX=15 |
|||
-D PIN_GPS_TX=12 |
|||
-D GPS_BAUD_RATE=9600 |
|||
-D P_LORA_DIO_1=33 |
|||
-D P_LORA_NSS=18 |
|||
-D P_LORA_RESET=23 |
|||
-D P_LORA_BUSY=32 |
|||
-D P_LORA_SCLK=5 |
|||
-D P_LORA_MOSI=27 |
|||
-D P_LORA_MISO=19 |
|||
-D SX126X_TXEN=13 |
|||
-D SX126X_RXEN=14 |
|||
-D PIN_BOARD_SDA=21 |
|||
-D PIN_BOARD_SCL=22 |
|||
-D SX126X_DIO2_AS_RF_SWITCH=false |
|||
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 |
|||
-D SX126X_CURRENT_LIMIT=140 |
|||
-D LORA_TX_POWER=22 |
|||
build_src_filter = ${esp32_base.build_src_filter} |
|||
+<../variants/hammer> |
|||
lib_deps = |
|||
${esp32_base.lib_deps} |
|||
stevemarple/MicroNMEA @ ^2.0.6 |
|||
adafruit/Adafruit SSD1306 @ ^2.5.13 |
|||
|
|||
|
|||
; ============================================================ |
|||
; REPEATER |
|||
; ============================================================ |
|||
[env:hammer_sx1262_repeater] |
|||
extends = hammer |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<../examples/simple_repeater/*.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D ADVERT_NAME='"Hammer Repeater"' |
|||
-D ADMIN_PASSWORD='"password"' |
|||
-D MAX_NEIGHBOURS=50 |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
${esp32_ota.lib_deps} |
|||
|
|||
|
|||
; ============================================================ |
|||
; ROOM SERVER |
|||
; ============================================================ |
|||
[env:hammer_sx1262_room_server] |
|||
extends = hammer |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<../examples/simple_room_server/*.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D ADVERT_NAME='"Hammer Room"' |
|||
-D ADMIN_PASSWORD='"password"' |
|||
-D ROOM_PASSWORD='"hello"' |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
${esp32_ota.lib_deps} |
|||
|
|||
|
|||
; ============================================================ |
|||
; COMPANION RADIO - USB (serial connection to phone/PC) |
|||
; ============================================================ |
|||
[env:hammer_sx1262_companion_usb] |
|||
extends = hammer |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<helpers/esp32/*.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
+<../examples/companion_radio/*.cpp> |
|||
+<../examples/companion_radio/ui-new/*.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-I examples/companion_radio/ui-new |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D MAX_CONTACTS=160 |
|||
-D MAX_GROUP_CHANNELS=8 |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
densaugeo/base64 @ ~1.4.0 |
|||
|
|||
|
|||
; ============================================================ |
|||
; COMPANION RADIO - BLE (Bluetooth connection to phone) |
|||
; ============================================================ |
|||
[env:hammer_sx1262_companion_ble] |
|||
extends = hammer |
|||
board_build.partitions = huge_app.csv |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<helpers/esp32/*.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
+<../examples/companion_radio/*.cpp> |
|||
+<../examples/companion_radio/ui-new/*.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-I examples/companion_radio/ui-new |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D MAX_CONTACTS=160 |
|||
-D MAX_GROUP_CHANNELS=8 |
|||
-D BLE_PIN_CODE=123456 |
|||
-D OFFLINE_QUEUE_SIZE=256 |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
densaugeo/base64 @ ~1.4.0 |
|||
|
|||
|
|||
; ============================================================ |
|||
; COMPANION RADIO - ETHERNET (TCP connection over W5500) |
|||
; ============================================================ |
|||
[env:hammer_sx1262_companion_ethernet] |
|||
extends = hammer |
|||
board_build.partitions = huge_app.csv |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<helpers/esp32/*.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
+<../examples/companion_radio/*.cpp> |
|||
+<../examples/companion_radio/ui-new/*.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-I examples/companion_radio/ui-new |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D MAX_CONTACTS=160 |
|||
-D MAX_GROUP_CHANNELS=8 |
|||
-D HAS_ETHERNET=1 |
|||
-D ETH_TCP_PORT=4403 |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
densaugeo/base64 @ ~1.4.0 |
|||
sstaub/Ethernet3 @ ^1.6.0 |
|||
|
|||
|
|||
; ============================================================ |
|||
; REPEATER + ESPNow BRIDGE |
|||
; ============================================================ |
|||
[env:hammer_sx1262_repeater_bridge_espnow] |
|||
extends = hammer |
|||
build_src_filter = ${hammer.build_src_filter} |
|||
+<helpers/bridges/ESPNowBridge.cpp> |
|||
+<helpers/ui/SSD1306Display.cpp> |
|||
+<helpers/ui/MomentaryButton.cpp> |
|||
+<../examples/simple_repeater/*.cpp> |
|||
build_flags = |
|||
${hammer.build_flags} |
|||
-D RADIO_CLASS=CustomSX1262 |
|||
-D WRAPPER_CLASS=CustomSX1262Wrapper |
|||
-D DISPLAY_CLASS=SSD1306Display |
|||
-D ADVERT_NAME='"Hammer ESPNow Bridge"' |
|||
-D ADMIN_PASSWORD='"password"' |
|||
-D MAX_NEIGHBOURS=50 |
|||
-D WITH_ESPNOW_BRIDGE=1 |
|||
lib_deps = |
|||
${hammer.lib_deps} |
|||
${esp32_ota.lib_deps} |
|||
@ -0,0 +1,150 @@ |
|||
#include <Arduino.h> |
|||
#include "target.h" |
|||
#include "variant.h" |
|||
|
|||
#if ENV_INCLUDE_GPS |
|||
#include <helpers/sensors/MicroNMEALocationProvider.h> |
|||
#endif |
|||
|
|||
ESP32Board board; |
|||
|
|||
SPIClass vspi(VSPI); // LoRa and Eth share VSPI
|
|||
|
|||
#if defined(P_LORA_SCLK) |
|||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, vspi); |
|||
#else |
|||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); |
|||
#endif |
|||
|
|||
WRAPPER_CLASS radio_driver(radio, board); |
|||
|
|||
ESP32RTCClock fallback_clock; |
|||
AutoDiscoverRTCClock rtc_clock(fallback_clock); |
|||
|
|||
#if ENV_INCLUDE_GPS |
|||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); |
|||
HammerSensorManager sensors = HammerSensorManager(nmea); |
|||
#else |
|||
HammerSensorManager sensors; |
|||
#endif |
|||
|
|||
#ifdef DISPLAY_CLASS |
|||
DISPLAY_CLASS display; |
|||
MomentaryButton user_btn(BUTTON_PIN); |
|||
#endif |
|||
|
|||
#ifdef HAS_ETHERNET |
|||
SerialEthernetInterface eth_interface; |
|||
#endif |
|||
|
|||
bool radio_init() { |
|||
fallback_clock.begin(); |
|||
rtc_clock.begin(Wire); |
|||
|
|||
#if defined(P_LORA_SCLK) |
|||
vspi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); |
|||
if (!radio.std_init(&vspi)) return false; |
|||
#else |
|||
if (!radio.std_init()) return false; |
|||
#endif |
|||
|
|||
#ifdef HAS_ETHERNET |
|||
// Generate unique MAC from ESP32 chip ID
|
|||
uint64_t chipid = ESP.getEfuseMac(); |
|||
uint8_t mac[6]; |
|||
mac[0] = 0xDE; mac[1] = 0xAD; |
|||
mac[2] = (chipid >> 32) & 0xFF; |
|||
mac[3] = (chipid >> 24) & 0xFF; |
|||
mac[4] = (chipid >> 16) & 0xFF; |
|||
mac[5] = (chipid >> 8) & 0xFF; |
|||
// vspi already initialized above — pass it to share the bus
|
|||
if (eth_interface.begin(vspi, ETH_TCP_PORT, ETH_CS_PIN, ETH_RST_PIN, mac)) { |
|||
eth_interface.enable(); |
|||
} |
|||
#endif |
|||
|
|||
return true; |
|||
} |
|||
|
|||
uint32_t radio_get_rng_seed() { return radio.random(0x7FFFFFFF); } |
|||
|
|||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { |
|||
radio.setFrequency(freq); |
|||
radio.setSpreadingFactor(sf); |
|||
radio.setBandwidth(bw); |
|||
radio.setCodingRate(cr); |
|||
} |
|||
|
|||
void radio_set_tx_power(uint8_t dbm) { radio.setOutputPower(dbm); } |
|||
|
|||
mesh::LocalIdentity radio_new_identity() { |
|||
RadioNoiseListener rng(radio); |
|||
return mesh::LocalIdentity(&rng); |
|||
} |
|||
|
|||
void HammerSensorManager::start_gps() { |
|||
if (!gps_active) { |
|||
_location->begin(); |
|||
gps_active = true; |
|||
} |
|||
} |
|||
|
|||
void HammerSensorManager::stop_gps() { |
|||
if (gps_active) { |
|||
gps_active = false; |
|||
_location->stop(); |
|||
} |
|||
} |
|||
|
|||
bool HammerSensorManager::begin() { |
|||
Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); |
|||
return true; |
|||
} |
|||
|
|||
bool HammerSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { |
|||
if (requester_permissions & TELEM_PERM_LOCATION) { |
|||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
void HammerSensorManager::loop() { |
|||
static long next_gps_update = 0; |
|||
|
|||
if (gps_active) { |
|||
_location->loop(); |
|||
} |
|||
|
|||
if (millis() > next_gps_update) { |
|||
if (gps_active && _location->isValid()) { |
|||
node_lat = ((double)_location->getLatitude()) / 1000000.0; |
|||
node_lon = ((double)_location->getLongitude()) / 1000000.0; |
|||
node_altitude = ((double)_location->getAltitude()) / 1000.0; |
|||
} |
|||
next_gps_update = millis() + 1000; |
|||
} |
|||
} |
|||
|
|||
int HammerSensorManager::getNumSettings() const { |
|||
return 1; |
|||
} |
|||
|
|||
const char* HammerSensorManager::getSettingName(int i) const { |
|||
return i == 0 ? "gps" : NULL; |
|||
} |
|||
|
|||
const char* HammerSensorManager::getSettingValue(int i) const { |
|||
return i == 0 ? (gps_active ? "1" : "0") : NULL; |
|||
} |
|||
|
|||
bool HammerSensorManager::setSettingValue(const char* name, const char* value) { |
|||
if (strcmp(name, "gps") == 0) { |
|||
if (strcmp(value, "0") == 0) { |
|||
stop_gps(); |
|||
} else { |
|||
start_gps(); |
|||
} |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
#pragma once |
|||
|
|||
#define RADIOLIB_STATIC_ONLY 1 |
|||
#include <RadioLib.h> |
|||
#include <helpers/radiolib/RadioLibWrappers.h> |
|||
#include <helpers/ESP32Board.h> |
|||
#include <helpers/radiolib/CustomSX1262Wrapper.h> |
|||
#include <helpers/radiolib/CustomSX1268Wrapper.h> |
|||
#include <helpers/AutoDiscoverRTCClock.h> |
|||
#include <helpers/SensorManager.h> |
|||
#include <helpers/sensors/LocationProvider.h> |
|||
|
|||
#ifdef DISPLAY_CLASS |
|||
#include <helpers/ui/SSD1306Display.h> |
|||
#include <helpers/ui/MomentaryButton.h> |
|||
#endif |
|||
|
|||
#ifdef HAS_ETHERNET |
|||
#include <helpers/esp32/SerialEthernetInterface.h> |
|||
#endif |
|||
|
|||
class HammerSensorManager : public SensorManager { |
|||
bool gps_active = false; |
|||
LocationProvider* _location; |
|||
|
|||
void start_gps(); |
|||
void stop_gps(); |
|||
|
|||
public: |
|||
HammerSensorManager(LocationProvider& location) : _location(&location) { } |
|||
bool begin() override; |
|||
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; |
|||
void loop() override; |
|||
int getNumSettings() const override; |
|||
const char* getSettingName(int i) const override; |
|||
const char* getSettingValue(int i) const override; |
|||
bool setSettingValue(const char* name, const char* value) override; |
|||
LocationProvider* getLocationProvider() override { return _location; } |
|||
}; |
|||
|
|||
extern ESP32Board board; |
|||
extern SPIClass vspi; |
|||
extern WRAPPER_CLASS radio_driver; |
|||
extern AutoDiscoverRTCClock rtc_clock; |
|||
extern HammerSensorManager sensors; |
|||
|
|||
#ifdef DISPLAY_CLASS |
|||
extern DISPLAY_CLASS display; |
|||
extern MomentaryButton user_btn; |
|||
#endif |
|||
|
|||
#ifdef HAS_ETHERNET |
|||
extern SerialEthernetInterface eth_interface; |
|||
#endif |
|||
|
|||
bool radio_init(); |
|||
uint32_t radio_get_rng_seed(); |
|||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); |
|||
void radio_set_tx_power(uint8_t dbm); |
|||
mesh::LocalIdentity radio_new_identity(); |
|||
@ -0,0 +1,62 @@ |
|||
// Hammer Board Variant Header
|
|||
|
|||
// OLED
|
|||
#define I2C_SDA 21 |
|||
#define I2C_SCL 22 |
|||
#define HAS_SCREEN |
|||
#define SCREEN_WIDTH 128 |
|||
#define SCREEN_HEIGHT 64 |
|||
#define OLED_RESET -1 // No reset pin
|
|||
#define OLED_I2C_ADDR 0x3C |
|||
|
|||
// GPS (u-blox)
|
|||
#define GPS_RX_PIN 15 |
|||
#define GPS_TX_PIN 12 |
|||
#define GPS_UBLOX |
|||
#define GPS_BAUDRATE 9600 |
|||
#define HAS_GPS |
|||
|
|||
// Power and Buttons
|
|||
#define EXT_PWR_DETECT 4 |
|||
#define BUTTON_PIN 39 |
|||
#define SECOND_BUTTON_PIN 0 // Boot button often used as second
|
|||
#define BATTERY_PIN 35 |
|||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL |
|||
#define ADC_MULTIPLIER 1.85 |
|||
|
|||
// LoRa (E22 on VSPI)
|
|||
#define USE_SX1262 // Primary for E22-900M30S
|
|||
#define USE_SX1268 // Optional for E22-400M30S
|
|||
#define SX126X_MAX_POWER 22 |
|||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8 |
|||
#define TCXO_OPTIONAL |
|||
|
|||
#define SX126X_CS 18 |
|||
#define SX126X_SCK 5 |
|||
#define SX126X_MOSI 27 |
|||
#define SX126X_MISO 19 |
|||
#define SX126X_RESET 23 |
|||
#define SX126X_DIO1 33 |
|||
#define SX126X_BUSY 32 |
|||
#define SX126X_TXEN 13 |
|||
#define SX126X_RXEN 14 |
|||
|
|||
// Compatibility defines (for RadioLib)
|
|||
#define P_LORA_NSS SX126X_CS |
|||
#define P_LORA_SCLK SX126X_SCK |
|||
#define P_LORA_MOSI SX126X_MOSI |
|||
#define P_LORA_MISO SX126X_MISO |
|||
#define P_LORA_RESET SX126X_RESET |
|||
#define P_LORA_DIO_1 SX126X_DIO1 |
|||
#define P_LORA_BUSY SX126X_BUSY |
|||
|
|||
// Ethernet (W5500 on VSPI - shared with LoRa)
|
|||
// HAS_ETHERNET is defined per-variant in platformio.ini, not here
|
|||
#define ETH_PHY_TYPE ETH_PHY_W5500 |
|||
#define ETH_CS_PIN 16 |
|||
#define ETH_RST_PIN 17 |
|||
#define ETH_INT_PIN -1 // Not connected
|
|||
#define ETH_SPI_HOST VSPI_HOST // Shared with LoRa
|
|||
#define ETH_SCLK SX126X_SCK // 5
|
|||
#define ETH_MOSI SX126X_MOSI // 27
|
|||
#define ETH_MISO SX126X_MISO // 19
|
|||
Loading…
Reference in new issue