Skip to content

Commit 7beaaf3

Browse files
committed
indicator: RP2040 peripherals for the main firmware
The SenseCAP Indicator RP2040 co-processor serves as a generic peripheral bridge over a serial protobuf link (interdevice.proto): - FakeI2C implements TwoWire and tunnels write and read transactions, so the standard sensor drivers and the I2C scan work unmodified on the bridged second bus (WIRE1) - FakeUART forwards GPS NMEA to the regular GPS driver - SD card access with chunked file transfers, paged directory listings and card statistics; device-ui loads map tiles and map styles from the card behind the RP2040 - link at 2M baud with 4KB chunks, message structs kept off task stacks Log messages carrying their own bracket tag render it like a thread name. Replaces the earlier IndicatorSensor/COBS approach.
1 parent faf92f6 commit 7beaaf3

23 files changed

Lines changed: 1176 additions & 464 deletions

File tree

platformio.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ lib_deps =
127127
[device-ui_base]
128128
lib_deps =
129129
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
130-
https://github.com/meshtastic/device-ui/archive/effbb925a7cbd3ab794dfcc5fa16228217d18408.zip
130+
https://github.com/meshtastic/device-ui/archive/dcb45b7d5ab6df2fcc2d66e10df7c2c0fcf3ff51.zip
131131

132132
; Common libs for environmental measurements in telemetry module
133133
[environmental_base]

protobufs

src/RedirectablePrint.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
7878
if (!std::isprint(static_cast<unsigned char>(printBuf[f])) && printBuf[f] != '\n')
7979
printBuf[f] = '#';
8080
}
81+
// A leading "[Tag] " in the message simulates the thread name tag (used
82+
// by contexts without an OSThread, e.g. the device-ui task). Print it
83+
// before applying the level color so it renders like a real thread name.
84+
size_t tagLen = 0;
85+
if (printBuf[0] == '[') {
86+
const char *end = (const char *)memchr(printBuf, ']', len < 24 ? len : 24);
87+
if (end && (size_t)(end - printBuf) + 1 < len && end[1] == ' ')
88+
tagLen = end - printBuf + 2;
89+
}
90+
if (tagLen)
91+
Print::write(printBuf, tagLen);
8192
if (color && logLevel != nullptr) {
8293
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0)
8394
Print::write("\u001b[34m", 5);
@@ -88,7 +99,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
8899
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0)
89100
Print::write("\u001b[31m", 5);
90101
}
91-
len = Print::write(printBuf, len);
102+
len = tagLen + Print::write(printBuf + tagLen, len - tagLen);
92103
if (color && logLevel != nullptr) {
93104
Print::write("\u001b[0m", 4);
94105
}
@@ -161,7 +172,9 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
161172
#endif
162173
}
163174
auto thread = concurrency::OSThread::currentThread;
164-
if (thread) {
175+
// A message starting with "[Tag] " brings its own tag (e.g. device-ui
176+
// logging from the UI task, where currentThread is unrelated)
177+
if (thread && format[0] != '[') {
165178
print("[");
166179
// printf("%p ", thread);
167180
// assert(thread->ThreadName.length());

src/configuration.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
395395
#ifndef WIRE_INTERFACES_COUNT
396396
// Officially an NRF52 macro
397397
// Repurposed cross-platform to identify devices using Wire1
398-
#if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA)
398+
// The SenseCAP Indicator has a second bus bridged to the RP2040 (FakeI2C)
399+
#if defined(I2C_SDA1) || defined(PIN_WIRE1_SDA) || defined(SENSECAP_INDICATOR)
399400
#define WIRE_INTERFACES_COUNT 2
400401
#elif HAS_WIRE
401402
#define WIRE_INTERFACES_COUNT 1

src/detect/ScanI2CTwoWire.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#if defined(ARCH_PORTDUINO)
99
#include "linux/LinuxHardwareI2C.h"
1010
#endif
11+
#if defined(SENSECAP_INDICATOR)
12+
#include "mesh/comms/FakeI2C.h"
13+
#endif
1114
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32)
1215
#include "meshUtils.h" // vformat
1316

@@ -245,7 +248,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
245248

246249
#if WIRE_INTERFACES_COUNT == 2
247250
if (port == I2CPort::WIRE1) {
251+
#if defined(SENSECAP_INDICATOR)
252+
i2cBus = FakeWire; // WIRE1 is bridged to the RP2040
253+
#else
248254
i2cBus = &Wire1;
255+
#endif
249256
} else {
250257
#endif
251258
i2cBus = &Wire;
@@ -908,7 +915,9 @@ TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address)
908915
if (address.port == ScanI2C::I2CPort::WIRE) {
909916
return &Wire;
910917
} else {
911-
#if WIRE_INTERFACES_COUNT == 2
918+
#if defined(SENSECAP_INDICATOR)
919+
return FakeWire; // WIRE1 is bridged to the RP2040
920+
#elif WIRE_INTERFACES_COUNT == 2
912921
return &Wire1;
913922
#else
914923
return &Wire;

src/gps/GPS.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ template <typename T, std::size_t N> std::size_t array_count(const T (&)[N])
4646
#define GPS_SERIAL_PORT Serial1
4747
#endif
4848

49-
#if defined(ARCH_NRF52)
49+
#if defined(SENSECAP_INDICATOR)
50+
FakeUART *GPS::_serial_gps = FakeSerial;
51+
#elif defined(ARCH_NRF52)
5052
Uart *GPS::_serial_gps = &GPS_SERIAL_PORT;
5153
#elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32)
5254
HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
@@ -1428,6 +1430,7 @@ void GPS::publishUpdate()
14281430

14291431
int32_t GPS::runOnce()
14301432
{
1433+
#if !defined(SENSECAP_INDICATOR)
14311434
if (!GPSInitFinished) {
14321435
if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
14331436
LOG_INFO("GPS set to not-present. Skip probe");
@@ -1448,6 +1451,7 @@ int32_t GPS::runOnce()
14481451
GPSInitFinished = true;
14491452
publishUpdate();
14501453
}
1454+
#endif
14511455

14521456
// ======================== GPS_ACTIVE state ========================
14531457
// In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages.
@@ -1910,8 +1914,10 @@ std::unique_ptr<GPS> GPS::createGps()
19101914
} else
19111915
return nullptr;
19121916
#endif
1917+
#if !defined(SENSECAP_INDICATOR)
19131918
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
19141919
return nullptr;
1920+
#endif
19151921

19161922
auto new_gps = std::unique_ptr<GPS>(new GPS());
19171923
new_gps->rx_gpio = _rx_gpio;

src/gps/GPS.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#include "input/UpDownInterruptImpl1.h"
1414
#include "modules/PositionModule.h"
1515

16+
#ifdef SENSECAP_INDICATOR
17+
#include "mesh/comms/FakeUART.h"
18+
#endif
19+
1620
// Allow defining the polarity of the ENABLE output. default is active high
1721
#ifndef GPS_EN_ACTIVE
1822
#define GPS_EN_ACTIVE 1
@@ -219,7 +223,9 @@ class GPS : private concurrency::OSThread
219223
CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep);
220224

221225
/** If !NULL we will use this serial port to construct our GPS */
222-
#if defined(ARCH_RP2040)
226+
#if defined(SENSECAP_INDICATOR)
227+
static FakeUART *_serial_gps;
228+
#elif defined(ARCH_RP2040)
223229
static SerialUART *_serial_gps;
224230
#elif defined(ARCH_NRF52)
225231
static Uart *_serial_gps;

src/graphics/tftSetup.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,85 @@
1414
#include <thread>
1515
#endif
1616

17+
#ifdef SENSECAP_INDICATOR
18+
#include "graphics/map/RemoteSDService.h"
19+
#include "input/I2CKeyboardScanner.h"
20+
#include "mesh/IndicatorSerial.h"
21+
#include "mesh/comms/FakeI2C.h"
22+
23+
// Serves the UI map tiles from the SD card behind the RP2040, chunk-wise
24+
// over the interdevice link
25+
class IndicatorRemoteFS : public IRemoteFS
26+
{
27+
public:
28+
bool readChunk(const char *path, uint32_t offset, uint8_t *buf, uint32_t len, uint32_t *bytesRead,
29+
uint32_t *fileSize) override
30+
{
31+
if (!sensecapIndicator)
32+
return false;
33+
memset(&result, 0, sizeof(result));
34+
if (!sensecapIndicator->file_read(path, offset, len, &result) || !result.success)
35+
return false;
36+
uint32_t n = result.filedata.size;
37+
if (n > len)
38+
n = len;
39+
memcpy(buf, result.filedata.bytes, n);
40+
*bytesRead = n;
41+
// lv_fs positions are 32 bit, map tiles never come close
42+
*fileSize = (uint32_t)result.file_size;
43+
return true;
44+
}
45+
46+
bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) override
47+
{
48+
if (!sensecapIndicator)
49+
return false;
50+
memset(&result, 0, sizeof(result));
51+
return sensecapIndicator->file_write(path, offset, buf, len, create, &result) && result.success;
52+
}
53+
54+
bool sdInfo(RemoteSdInfo &info) override
55+
{
56+
if (!sensecapIndicator)
57+
return false;
58+
meshtastic_SdCardInfo result = meshtastic_SdCardInfo_init_zero;
59+
if (!sensecapIndicator->sd_info(&result))
60+
return false;
61+
info.present = result.present;
62+
info.cardType = (uint8_t)result.card_type;
63+
info.fatType = (uint8_t)result.fat_type;
64+
info.cardSize = result.card_size;
65+
info.usedBytes = result.used_bytes;
66+
info.freeBytes = result.free_bytes;
67+
return true;
68+
}
69+
70+
bool listDir(const char *path, std::set<std::string> &entries) override
71+
{
72+
if (!sensecapIndicator)
73+
return false;
74+
uint32_t offset = 0;
75+
while (true) {
76+
memset(&listing, 0, sizeof(listing));
77+
if (!sensecapIndicator->list_directory(path, offset, &listing) || !listing.success)
78+
return false;
79+
for (pb_size_t i = 0; i < listing.filenames_count; i++)
80+
entries.insert(listing.filenames[i]);
81+
offset += listing.filenames_count;
82+
if (listing.filenames_count == 0 || offset >= listing.total_count)
83+
break;
84+
}
85+
return true;
86+
}
87+
88+
private:
89+
// Several KB each, kept off the UI task stack. All file operations
90+
// originate from the single UI task.
91+
meshtastic_FileTransfer result;
92+
meshtastic_DirectoryListing listing;
93+
};
94+
#endif
95+
1796
DeviceScreen *deviceScreen = nullptr;
1897

1998
#ifndef TFT_TASK_STACK_SIZE
@@ -40,6 +119,12 @@ void tft_task_handler(void *param = nullptr)
40119

41120
void tftSetup(void)
42121
{
122+
#ifdef SENSECAP_INDICATOR
123+
RemoteSDService::setBackend(new IndicatorRemoteFS());
124+
// the second bus is bridged to the RP2040, keep the keyboard scan off the
125+
// uninitialized local Wire1
126+
I2CKeyboardScanner::setSecondaryBus(FakeWire);
127+
#endif
43128
#ifndef ARCH_PORTDUINO
44129
deviceScreen = &DeviceScreen::create();
45130
PacketAPI::create(PacketServer::init());

src/main.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
#include "detect/ScanI2C.h"
3131
#include "error.h"
3232

33+
#ifdef SENSECAP_INDICATOR // on the indicator run the additional serial port for the RP2040
34+
#include "IndicatorSerial.h"
35+
#include "mesh/comms/FakeI2C.h"
36+
#endif
37+
3338
#if !MESHTASTIC_EXCLUDE_I2C
3439
#include "detect/ScanI2CConsumer.h"
3540
#include "detect/ScanI2CTwoWire.h"
@@ -553,7 +558,10 @@ void setup()
553558
#endif
554559

555560
#if !MESHTASTIC_EXCLUDE_I2C
556-
#if defined(I2C_SDA1) && defined(ARCH_RP2040)
561+
#if defined(SENSECAP_INDICATOR)
562+
// The Sensecap Indicator has its second I2C bus on the RP2040, bridged
563+
// over serial as FakeWire. No local interface to initialize.
564+
#elif defined(I2C_SDA1) && defined(ARCH_RP2040)
557565
Wire1.setSDA(I2C_SDA1);
558566
Wire1.setSCL(I2C_SCL1);
559567
Wire1.begin();
@@ -616,6 +624,18 @@ void setup()
616624
mcp23017EarlyInit();
617625
#endif
618626

627+
#ifdef SENSECAP_INDICATOR
628+
// Power the RP2040 co-processor and start the interdevice link before the
629+
// I2C scan, so that its bus can be probed through the bridge
630+
#ifdef SENSOR_POWER_CTRL_EXPANDER
631+
pinMode(SENSOR_POWER_CTRL_EXPANDER, OUTPUT);
632+
digitalWrite(SENSOR_POWER_CTRL_EXPANDER, SENSOR_POWER_ON_EXPANDER);
633+
#endif
634+
sensecapIndicator = new SensecapIndicator(Serial2);
635+
if (!sensecapIndicator->wait_ready(3000))
636+
LOG_WARN("RP2040 co-processor not answering, bridged I2C bus unavailable");
637+
#endif
638+
619639
#if !MESHTASTIC_EXCLUDE_I2C
620640
// We need to scan here to decide if we have a screen for nodeDB.init() and because power has been applied to
621641
// accessories
@@ -624,7 +644,7 @@ void setup()
624644
LOG_INFO("Scan for i2c devices");
625645
#endif
626646

627-
#if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
647+
#if defined(SENSECAP_INDICATOR) || defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2))
628648
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1);
629649
#endif
630650

0 commit comments

Comments
 (0)