firmware/src/modules/SerialModule.cpp
Jonathan Bennett 4feaec651f
Unify the native display config between legacy display and MUI (#6838)
* Add missed include

* Another Warning fix

* Add another HAS_SCREEN

* Namespace fixes

* Removed depricated destination types and re-factored destination screen

* Get rid of Arduino Strings

* Clean up after Copilot

* SixthLine Def, Screen Rename

Added Sixth Line Definition Screen Rename, and Automatic Line Adjustment

* Consistency is hard - fixed "Sixth"

* System Frame Updates

Adjusted line construction to ensure we fit maximum content per screen.

* Fix up notifications

* Add a couple more ifdef HAS_SCREEN lines

* Add screen->isOverlayBannerShowing()

* Don't forget the invert!

* Adjust Nodelist Center Divider

Adjust Nodelist Center Divider

* Fix variable casting

* Fix entryText variable as empty before update to fix validation

* Altitude is int32_t

* Update PowerTelemetry to have correct data type

* Fix cppcheck warnings (#6945)

* Fix cppcheck warnings

* Adjust logic in Power.cpp for power sensor

---------

Co-authored-by: Jason P <applewiz@mac.com>

* More pixel wrangling so things line up NodeList edition

* Adjust NodeList alignments and plumb some background padding for a possible title fix

* Better alignment for banner notifications

* Move title into drawCommonHeader; initial screen tested

* Fonts make spacing items difficult

* Improved beeping booping and other buzzer based feedback (#6947)

* Improved beeping booping and other buzzer based feedback

* audible button feedback (#6949)

* Refactor

---------

Co-authored-by: todd-herbert <herbert.todd@gmail.com>

* Sandpapered the corners of the notification popup

* Finalize drawCommonHeader migration

* Update Title of Favorite Node Screens

* Update node metric alignment on LoRa screen

* Update the border for popups to separate it from background

* Update PaxcounterModule.cpp with CommonHeader

* Update WiFi screen with CommonHeader and related data reflow

* It was not, in fact, pointing up

* Fix build on wismeshtap

* T-deck trackball debounce

* Fix uptime on Device Focused page to actually detail

* Update Sys screen for new uptime, add label to Freq/Chan on LoRa

* Don't display DOP any longer, make Uptime consistent

* Revert Uptime change on Favorites, Apply to Device Focused

* Label the satelite number to avoid confusion

* Boop boop boop boop

* Correct GPS positioning and string consistency across strings for GPS

* Fix GPS text alignment

* Enable canned messages by default

* Don't wake screen on new nodes

* Cannedmessage list emote support added

* Fn+e emote picker for freetext screen

* Actually block CannedInput actions while display is shown

* Add selection menu to bannerOverlay

* Off by one

* Move to unified text layouts and spacing

* Still my Fav without an "e"

* Fully remove EVENT_NODEDB_UPDATED

* Simply LoRa screen

* Make some char pointers const to fix compilation on native targets

* Update drawCompassNorth to include radius

* Fix warning

* button thread cleanup

* Pull OneButton handling from PowerFSM and add MUI switch (#6973)

* Trunk

* Onebutton Menu Support

* Add temporary clock icon

* Add gps location to fsi

* Banner message state reset

* Cast to char to satisfy compiler

* Better fast handling of input during banner

* Fix warning

* Derp

* oops

* Update ref

* Wire buzzer_mode

* remove legacy string->print()

* Only init screen if one found

* Unsigned Char

* More buttonThread cleaning

* screen.cpp button handling cleanup

* The Great Event Rename of 2025

* Fix the Radiomaster

* Missed trackball type change

* Remove unused function

* Make ButtonThread an InputBroker

* Coffee hadn't kicked in yet

* Add clock icon for Navigation Bar

* Restore clock screen definition code - whoops

* ExternalNotifications now observe inputBroker

* Clock rework (#6992)

* Move Clock bits into ClockRenderer space

* Rework clock into all device navigation

* T-Watch Actually Builds Different

* Compile fix

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* Add AM/PM to Digital Clock

* Flip Seconds and AM/PM on Clock Display

* Tik-tok pixels are hard

* Fix builds on Thinknode M1

* Check for GPS and don't crash

* Don't endif til the end

* Rework the OneButton thread to be much less of a mess. (#6997)

* Rework the OneButton thread to be much less of a mess. And break lots of targets temporarily

* Update src/input/ButtonThread.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix GPS toggle

* Send the shutdown event, not just the kbchar

* Honor the back button in a notificaiton popup

* Draw the right size box for popup with options

* Try to un-break all the things

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* 24-hour Clock Should have leading zero, but not 12-hour

* Fixup some compile errors

* Add intRoutine to ButtonThread init, to get more responsive user button back

* Add Timezone picker

* Fix Warning

* Optionally set the initial selection for the chooser popup

* Make back buttons work in canned messages

* Drop the wrapper classes

* LonPressTime now configurable

* Clock Frame can not longer be blank; just add valid time

* Back buttons everywhere!

* Key Verification confirm banner

* Make Elecrow M* top button a back button

* Add settings saves

* EInk responsiveness fixes

* Linux Input Fixes

* Add Native Trackball/Joystick support, and move UserButton to Input

* No Flight Stick Mode

* Send input event

* Add Channel Utilization to Device Focused frame

* Don't shift screens when we draw new ones

* Add showOverlayBanner arguments to no-op

* trunk

* Default Native trackball to NC

* Fix crash in simulator mode

* Add longLong button press

* Get the args right

* Adjust Bluetooth Pairing Screen to account for bottom navigation.

* Trackball everywhere, and unPhone buttons

* Remap visionmaster secondary button to TB_UP

* Kill ScanAndSelect

* trunk

* No longer need the canned messages input filter

* All Canned All the time

* Fix stm32 compile error regarding inputBroker

* Unify tft lineheights (#7033)

* Create variable line heights based upon SCREEN_HEIGHT

* Refactor textPositions into method -> getTextPositions

* Update SharedUIDisplay.h

---------

Co-authored-by: Jason P <applewiz@mac.com>

* Adjust top distance for larger displays

* Adjust icon sizes for larger displays

* Fix Paxcounter compile errors after code updates

* Pixel wrangling to make larger screens fit better

* Alert frame has precedence over banner -- for now

* Unify on ALT_BUTTON

* Align AM/PM to the digit, not the segment on larger displays

* Move some global pin defines into configuration.h

* Scaffolding for BMM150 9-axis gyro

* Alt button behavior

* Don't add the blank GPS frames without HAS_GPS

* EVENT_NODEDB_UPDATED has been retired

* Clean out LOG_WARN messages from debugging

* Add dismiss message function

* Minor buttonThread cleanup

* Add BMM150 support

* Clean up last warning from dev

* Simplify bmm150 init return logic

* Add option to reply to messages

* Add minimal menu upon selecting home screen

* Move Messages to slot 2, rename GPS to Position, move variables nearer functional usage in Screen.cpp

* Properly dismiss message

* T-Deck Trackball press is not user button

* Add select on favorite frame to launch cannedMessage DM

* Minor wording change

* Less capital letters

* Fix empty message check, time isn't reliable

* drop dead code

* Make UIRenderer a static class instead of namespace

* Fix the select on favorite

* Check if message is empty early and then 'return'

* Add kb_found, and show the option to launch freetype if appropriate

* Ignore impossible touchscreen touches

* Auto scroll fix

* Move linebreak after "from" for banners to maximize screen usage.

* Center "No messages to show" on Message frame

* Start consolidating buzzer behavior

* Fixed signed / unsigned warning

* Cast second parameter of max() to make some targets happy

* Cast kbchar to (char) to make arduino string happy

* Shorten the notice of "No messages"

* Add buzzer mode chooser

* Add regionPicker to Lora icon

* Reduce line spacing and reorder Position screen to resolve overlapping issues

* Update message titles, fix GPS icons, add Back options

* Leftover boops

* Remove chirp

* Make the region selection dismissable when a region is already set

* Add read-aloud functionality on messages w/ esp8266sam

* "Last Heard" is a better label

* tweak the beep

* 5 options

* properly tear down freetext upon cancel

* de-convelute canned messages just a bit

* Correct height of Mail icon in navigation bar

* Remove unused warning

* Consolidate time methods into TimeFormatters

* Oops

* Change LoRa Picker Cancel to Back

* Tweak selection characters on Banner

* Message render not scrolling on 5th line

* More fixes for message scrolling

* Remove the safety next on text overflow - we found that root cause

* Add pin definitions to fix compilation for obscure target

* Don't let the touchscreen send unitialized kbchar values

* Make virtual KB just a bit quicker

* No more double tap, swipe!

* Left is left, and Right is right

* Update horizontal lightning bolt design

* Move from solid to dashed separator for Message Frame

* Single emote feature fix

* Manually sort overlapping elements for now

* Freetext and clearer choices

* Fix ESP32 InkHUD builds on the unify-tft branch (#7087)

* Remove BaseUI branding

* Capitalization is fun

* Revert Meshtastic Boot Frame Changes

* Add ANZ_433 LoRa region to picker

* Update settings.json

---------

Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-21 06:36:04 -05:00

657 lines
27 KiB
C++

#include "SerialModule.h"
#include "GeoCoord.h"
#include "MeshService.h"
#include "NMEAWPL.h"
#include "NodeDB.h"
#include "RTC.h"
#include "Router.h"
#include "configuration.h"
#include <Arduino.h>
#include <Throttle.h>
/*
SerialModule
A simple interface to send messages over the mesh network by sending strings
over a serial port.
There are no PIN defaults, you have to enable the second serial port yourself.
Need help with this module? Post your question on the Meshtastic Discourse:
https://meshtastic.discourse.group
Basic Usage:
1) Enable the module by setting enabled to 1.
2) Set the pins (rxd / rxd) for your preferred RX and TX GPIO pins.
On tbeam, recommend to use:
RXD 35
TXD 15
3) Set timeout to the amount of time to wait before we consider
your packet as "done".
4) not applicable any more
5) Connect to your device over the serial interface at 38400 8N1.
6) Send a packet up to 240 bytes in length. This will get relayed over the mesh network.
7) (Optional) Set echo to 1 and any message you send out will be echoed back
to your device.
TODO (in this order):
* Define a verbose RX mode to report on mesh and packet information.
- This won't happen any time soon.
KNOWN PROBLEMS
* Until the module is initialized by the startup sequence, the TX pin is in a floating
state. Device connected to that pin may see this as "noise".
* Will not work on Linux device targets.
*/
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
!defined(CONFIG_IDF_TARGET_ESP32C3)
#define RX_BUFFER 256
#define TIMEOUT 250
#define BAUD 38400
#define ACK 1
// API: Defaulting to the formerly removed phone_timeout_secs value of 15 minutes
#define SERIAL_CONNECTION_TIMEOUT (15 * 60) * 1000UL
SerialModule *serialModule;
SerialModuleRadio *serialModuleRadio;
#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1)
SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {}
static Print *serialPrint = &Serial;
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") {}
static Print *serialPrint = &Serial1;
#else
SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") {}
static Print *serialPrint = &Serial2;
#endif
char serialBytes[512];
size_t serialPayloadSize;
SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio")
{
switch (moduleConfig.serial.mode) {
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG:
ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP;
break;
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA:
case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO:
ourPortNum = meshtastic_PortNum_POSITION_APP;
break;
default:
ourPortNum = meshtastic_PortNum_SERIAL_APP;
// restrict to the serial channel for rx
boundChannel = Channels::serialChannel;
break;
}
}
/**
* @brief Checks if the serial connection is established.
*
* @return true if the serial connection is established, false otherwise.
*
* For the serial2 port we can't really detect if any client is on the other side, so instead just look for recent messages
*/
bool SerialModule::checkIsConnected()
{
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
}
int32_t SerialModule::runOnce()
{
/*
Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI.
*/
// moduleConfig.serial.enabled = true;
// moduleConfig.serial.rxd = 35;
// moduleConfig.serial.txd = 15;
// moduleConfig.serial.override_console_serial_port = true;
// moduleConfig.serial.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO;
// moduleConfig.serial.timeout = 1000;
// moduleConfig.serial.echo = 1;
if (!moduleConfig.serial.enabled)
return disable();
if (moduleConfig.serial.override_console_serial_port || (moduleConfig.serial.rxd && moduleConfig.serial.txd)) {
if (firstTime) {
// Interface with the serial peripheral from in here.
LOG_INFO("Init serial peripheral interface");
uint32_t baud = getBaudRate();
if (moduleConfig.serial.override_console_serial_port) {
#ifdef RP2040_SLOW_CLOCK
Serial2.flush();
serialPrint = &Serial2;
#else
Serial.flush();
serialPrint = &Serial;
#endif
// Give it a chance to flush out 💩
delay(10);
}
#if defined(CONFIG_IDF_TARGET_ESP32C6)
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
Serial1.setRxBufferSize(RX_BUFFER);
Serial1.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd);
} else {
Serial.begin(baud);
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
}
#elif defined(ARCH_ESP32)
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
Serial2.setRxBufferSize(RX_BUFFER);
Serial2.begin(baud, SERIAL_8N1, moduleConfig.serial.rxd, moduleConfig.serial.txd);
} else {
Serial.begin(baud);
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
}
#elif !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1)
if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
#ifdef ARCH_RP2040
Serial2.setFIFOSize(RX_BUFFER);
Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd);
#else
Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd);
#endif
Serial2.begin(baud, SERIAL_8N1);
Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
} else {
#ifdef RP2040_SLOW_CLOCK
Serial2.begin(baud, SERIAL_8N1);
Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
#else
Serial.begin(baud, SERIAL_8N1);
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
#endif
}
#else
Serial.begin(baud, SERIAL_8N1);
Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
#endif
serialModuleRadio = new SerialModuleRadio();
firstTime = 0;
// in API mode send rebooted sequence
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
emitRebooted();
}
} else {
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
return runOncePart();
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) {
// in NMEA mode send out GGA every 2 seconds, Don't read from Port
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) {
lastNmeaTime = millis();
printGGA(outbuf, sizeof(outbuf), localPosition);
serialPrint->printf("%s", outbuf);
}
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) {
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) {
lastNmeaTime = millis();
uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) {
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true);
serialPrint->printf("%s", outbuf);
}
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
}
}
}
#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1)
else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
processWXSerial();
} else {
#if defined(CONFIG_IDF_TARGET_ESP32C6)
while (Serial1.available()) {
serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
#else
while (Serial2.available()) {
serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
#endif
serialModuleRadio->sendPayload();
}
}
#endif
}
return (10);
} else {
return disable();
}
}
/**
* Sends telemetry packet over the mesh network.
*
* @param m The telemetry data to be sent
*
* @return void
*
* @throws None
*/
void SerialModule::sendTelemetry(meshtastic_Telemetry m)
{
meshtastic_MeshPacket *p = router->allocForSending();
p->decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
p->decoded.payload.size =
pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Telemetry_msg, &m);
p->to = NODENUM_BROADCAST;
p->decoded.want_response = false;
if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
p->want_ack = true;
p->priority = meshtastic_MeshPacket_Priority_HIGH;
} else {
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
}
service->sendToMesh(p, RX_SRC_LOCAL, true);
}
/**
* Allocates a new mesh packet for use as a reply to a received packet.
*
* @return A pointer to the newly allocated mesh packet.
*/
meshtastic_MeshPacket *SerialModuleRadio::allocReply()
{
auto reply = allocDataPacket(); // Allocate a packet for sending
return reply;
}
/**
* Sends a payload to a specified destination node.
*
* @param dest The destination node number.
* @param wantReplies Whether or not to request replies from the destination node.
*/
void SerialModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
{
const meshtastic_Channel *ch = (boundChannel != NULL) ? &channels.getByName(boundChannel) : NULL;
meshtastic_MeshPacket *p = allocReply();
p->to = dest;
if (ch != NULL) {
p->channel = ch->index;
}
p->decoded.want_response = wantReplies;
p->want_ack = ACK;
p->decoded.payload.size = serialPayloadSize; // You must specify how many bytes are in the reply
memcpy(p->decoded.payload.bytes, serialBytes, p->decoded.payload.size);
service->sendToMesh(p);
}
/**
* Handle a received mesh packet.
*
* @param mp The received mesh packet.
* @return The processed message.
*/
ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp)
{
if (moduleConfig.serial.enabled) {
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) {
// in API mode we don't care about stuff from radio.
return ProcessMessage::CONTINUE;
}
auto &p = mp.decoded;
// LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s",
// nodeDB->getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes);
if (isFromUs(&mp)) {
/*
* If moduleConfig.serial.echo is true, then echo the packets that are sent out
* back to the TX of the serial interface.
*/
if (moduleConfig.serial.echo) {
// For some reason, we get the packet back twice when we send out of the radio.
// TODO: need to find out why.
if (lastRxID != mp.id) {
lastRxID = mp.id;
// LOG_DEBUG("* * Message came this device");
// serialPrint->println("* * Message came this device");
serialPrint->printf("%s", p.payload.bytes);
}
}
} else {
if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT ||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) {
serialPrint->write(p.payload.bytes, p.payload.size);
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
serialPrint->println();
serialPrint->printf("%s: %s", sender, p.payload.bytes);
serialPrint->println();
} else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA ||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) &&
HAS_GPS) {
// Decode the Payload some more
meshtastic_Position scratch;
meshtastic_Position *decoded = NULL;
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) {
memset(&scratch, 0, sizeof(scratch));
if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) {
decoded = &scratch;
}
// send position packet as WPL to the serial port
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name,
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
serialPrint->printf("%s", outbuf);
}
}
}
}
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
}
/**
* @brief Returns the baud rate of the serial module from the module configuration.
*
* @return uint32_t The baud rate of the serial module.
*/
uint32_t SerialModule::getBaudRate()
{
if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_110) {
return 110;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_300) {
return 300;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_600) {
return 600;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_1200) {
return 1200;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_2400) {
return 2400;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_4800) {
return 4800;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_9600) {
return 9600;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_19200) {
return 19200;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_38400) {
return 38400;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_57600) {
return 57600;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_115200) {
return 115200;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_230400) {
return 230400;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_460800) {
return 460800;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_576000) {
return 576000;
} else if (moduleConfig.serial.baud == meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600) {
return 921600;
}
return BAUD;
}
// Add this structure to help with parsing WindGust = 24.4 serial lines.
struct ParsedLine {
char name[64];
char value[128];
};
/**
* Parse a line of format "Name = Value" into name/value pair
* @param line Input line to parse
* @return ParsedLine containing name and value, or empty strings if parse failed
*/
ParsedLine parseLine(const char *line)
{
ParsedLine result = {"", ""};
// Find equals sign
const char *equals = strchr(line, '=');
if (!equals) {
return result;
}
// Extract name by copying substring
char nameBuf[64]; // Temporary buffer
size_t nameLen = equals - line;
if (nameLen >= sizeof(nameBuf)) {
nameLen = sizeof(nameBuf) - 1;
}
strncpy(nameBuf, line, nameLen);
nameBuf[nameLen] = '\0';
// Trim whitespace from name
char *nameStart = nameBuf;
while (*nameStart && isspace(*nameStart))
nameStart++;
char *nameEnd = nameStart + strlen(nameStart) - 1;
while (nameEnd > nameStart && isspace(*nameEnd))
*nameEnd-- = '\0';
// Copy trimmed name
strncpy(result.name, nameStart, sizeof(result.name) - 1);
result.name[sizeof(result.name) - 1] = '\0';
// Extract value part (after equals)
const char *valueStart = equals + 1;
while (*valueStart && isspace(*valueStart))
valueStart++;
strncpy(result.value, valueStart, sizeof(result.value) - 1);
result.value[sizeof(result.value) - 1] = '\0';
// Trim trailing whitespace from value
char *valueEnd = result.value + strlen(result.value) - 1;
while (valueEnd > result.value && isspace(*valueEnd))
*valueEnd-- = '\0';
return result;
}
/**
* Process the received weather station serial data, extract wind, voltage, and temperature information,
* calculate averages and send telemetry data over the mesh network.
*
* @return void
*/
void SerialModule::processWXSerial()
{
#if !defined(TTGO_T_ECHO) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && \
!defined(ELECROW_ThinkNode_M1)
static unsigned int lastAveraged = 0;
static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
static double dir_sum_sin = 0;
static double dir_sum_cos = 0;
static float velSum = 0;
static float gust = 0;
static float lull = -1;
static int velCount = 0;
static int dirCount = 0;
static char windDir[4] = "xxx"; // Assuming windDir is 3 characters long + null terminator
static char windVel[5] = "xx.x"; // Assuming windVel is 4 characters long + null terminator
static char windGust[5] = "xx.x"; // Assuming windGust is 4 characters long + null terminator
static char batVoltage[5] = "0.0V";
static char capVoltage[5] = "0.0V";
static char temperature[5] = "00.0";
static float batVoltageF = 0;
static float capVoltageF = 0;
static float temperatureF = 0;
static char rainStr[] = "5780860000";
static int rainSum = 0;
static float rain = 0;
bool gotwind = false;
while (Serial2.available()) {
// clear serialBytes buffer
memset(serialBytes, '\0', sizeof(serialBytes));
// memset(formattedString, '\0', sizeof(formattedString));
serialPayloadSize = Serial2.readBytes(serialBytes, 512);
// check for a strings we care about
// example output of serial data fields from the WS85
// WindDir = 79
// WindSpeed = 0.5
// WindGust = 0.6
// GXTS04Temp = 24.4
// Temperature = 23.4 // WS80
// RainIntSum = 0
// Rain = 0.0
if (serialPayloadSize > 0) {
// Define variables for line processing
int lineStart = 0;
int lineEnd = -1;
// Process each byte in the received data
for (size_t i = 0; i < serialPayloadSize; i++) {
// go until we hit the end of line and then process the line
if (serialBytes[i] == '\n') {
lineEnd = i;
// Extract the current line
char line[meshtastic_Constants_DATA_PAYLOAD_LEN];
memset(line, '\0', sizeof(line));
if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) {
memcpy(line, &serialBytes[lineStart], lineEnd - lineStart);
ParsedLine parsed = parseLine(line);
if (strlen(parsed.name) > 0) {
if (strcmp(parsed.name, "WindDir") == 0) {
strlcpy(windDir, parsed.value, sizeof(windDir));
double radians = GeoCoord::toRadians(strtof(windDir, nullptr));
dir_sum_sin += sin(radians);
dir_sum_cos += cos(radians);
dirCount++;
gotwind = true;
} else if (strcmp(parsed.name, "WindSpeed") == 0) {
strlcpy(windVel, parsed.value, sizeof(windVel));
float newv = strtof(windVel, nullptr);
velSum += newv;
velCount++;
if (newv < lull || lull == -1) {
lull = newv;
}
gotwind = true;
} else if (strcmp(parsed.name, "WindGust") == 0) {
strlcpy(windGust, parsed.value, sizeof(windGust));
float newg = strtof(windGust, nullptr);
if (newg > gust) {
gust = newg;
}
gotwind = true;
} else if (strcmp(parsed.name, "BatVoltage") == 0) {
strlcpy(batVoltage, parsed.value, sizeof(batVoltage));
batVoltageF = strtof(batVoltage, nullptr);
break; // last possible data we want so break
} else if (strcmp(parsed.name, "CapVoltage") == 0) {
strlcpy(capVoltage, parsed.value, sizeof(capVoltage));
capVoltageF = strtof(capVoltage, nullptr);
} else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) {
strlcpy(temperature, parsed.value, sizeof(temperature));
temperatureF = strtof(temperature, nullptr);
} else if (strcmp(parsed.name, "RainIntSum") == 0) {
strlcpy(rainStr, parsed.value, sizeof(rainStr));
rainSum = int(strtof(rainStr, nullptr));
} else if (strcmp(parsed.name, "Rain") == 0) {
strlcpy(rainStr, parsed.value, sizeof(rainStr));
rain = strtof(rainStr, nullptr);
}
}
// Update lineStart for the next line
lineStart = lineEnd + 1;
}
}
}
break;
// clear the input buffer
while (Serial2.available() > 0) {
Serial2.read(); // Read and discard the bytes in the input buffer
}
}
}
if (gotwind) {
LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr),
strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum);
}
if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) {
// calculate averages and send to the mesh
float velAvg = 1.0 * velSum / velCount;
double avgSin = dir_sum_sin / dirCount;
double avgCos = dir_sum_cos / dirCount;
double avgRadians = atan2(avgSin, avgCos);
float dirAvg = GeoCoord::toDegrees(avgRadians);
if (dirAvg < 0) {
dirAvg += 360.0;
}
lastAveraged = millis();
// make a telemetry packet with the data
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
m.which_variant = meshtastic_Telemetry_environment_metrics_tag;
m.variant.environment_metrics.wind_speed = velAvg;
m.variant.environment_metrics.has_wind_speed = true;
m.variant.environment_metrics.wind_direction = dirAvg;
m.variant.environment_metrics.has_wind_direction = true;
m.variant.environment_metrics.temperature = temperatureF;
m.variant.environment_metrics.has_temperature = true;
m.variant.environment_metrics.voltage =
capVoltageF > batVoltageF ? capVoltageF : batVoltageF; // send the larger of the two voltage values.
m.variant.environment_metrics.has_voltage = true;
m.variant.environment_metrics.wind_gust = gust;
m.variant.environment_metrics.has_wind_gust = true;
m.variant.environment_metrics.rainfall_24h = rainSum;
m.variant.environment_metrics.has_rainfall_24h = true;
// not sure if this value is actually the 1hr sum so needs to do some testing
m.variant.environment_metrics.rainfall_1h = rain;
m.variant.environment_metrics.has_rainfall_1h = true;
if (lull == -1)
lull = 0;
m.variant.environment_metrics.wind_lull = lull;
m.variant.environment_metrics.has_wind_lull = true;
LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f",
m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction,
m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust,
m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature);
sendTelemetry(m);
// reset counters and gust/lull
velSum = velCount = dirCount = 0;
dir_sum_sin = dir_sum_cos = 0;
gust = 0;
lull = -1;
}
#endif
return;
}
#endif