mirror of
https://github.com/m5stack/StackChan-BSP.git
synced 2026-05-20 11:51:35 -07:00
@@ -76,7 +76,7 @@ body:
|
||||
label: Issue checklist
|
||||
description: Please double-check that you have done each of the following things before submitting the issue.
|
||||
options:
|
||||
- label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/M5Stack/issues?q=)
|
||||
- label: I searched for previous reports in [the issue tracker](https://github.com/m5stack/StackChan-BSP/issues?q=)
|
||||
required: true
|
||||
- label: My report contains all necessary details
|
||||
required: true
|
||||
|
||||
@@ -39,12 +39,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
path:
|
||||
- check: './' # path to include
|
||||
exclude: '' # path to exclude
|
||||
#- check: 'src'
|
||||
# exclude: '(Fonts)' # Exclude file paths containing "Fonts"
|
||||
#- check: 'examples'
|
||||
# exclude: ''
|
||||
# - check: './' # path to include
|
||||
# exclude: '' # path to exclude
|
||||
- check: 'src'
|
||||
exclude: 'src/(utils|drivers)'
|
||||
- check: 'examples'
|
||||
exclude: ''
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Product Name
|
||||
# StackChan
|
||||
|
||||
## Overview
|
||||
|
||||
### SKU:xxx
|
||||
### SKU:K151
|
||||
|
||||
Description of the product
|
||||
|
||||
@@ -12,7 +12,9 @@ Description of the product
|
||||
|
||||
## Required Libraries:
|
||||
|
||||
- [Adafruit_BMP280_Library](https://github.com/adafruit/Required_Libraries_Link)
|
||||
- [M5Unified](https://github.com/m5stack/M5Unified)
|
||||
- [IRremoteESP8266](https://github.com/crankyoldgit/irremoteesp8266)
|
||||
- [M5Unit-NFC](https://github.com/m5stack/M5Unit-NFC)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
|
||||
/* Setup display */
|
||||
M5StackChan.Display().setTextSize(2);
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
M5StackChan.Display().setTextScroll(true);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
/* Get battery info from INA226 */
|
||||
float voltage = M5StackChan.getBatteryVoltage();
|
||||
float current = M5StackChan.getBatteryCurrent() * 1000;
|
||||
|
||||
M5StackChan.Display().printf("> Bat: %0.2fV %0.2fmA\n", voltage, current);
|
||||
|
||||
delay(1000);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// More examples on: https://github.com/crankyoldgit/IRremoteESP8266/tree/master/examples
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
#include <assert.h>
|
||||
#include <IRrecv.h>
|
||||
#include <IRremoteESP8266.h>
|
||||
#include <IRac.h>
|
||||
#include <IRtext.h>
|
||||
#include <IRutils.h>
|
||||
|
||||
const uint16_t kRecvPin = 10; // IR rx pin
|
||||
|
||||
const uint16_t kCaptureBufferSize = 1024;
|
||||
|
||||
#if DECODE_AC
|
||||
// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator
|
||||
// A value this large may swallow repeats of some protocols
|
||||
const uint8_t kTimeout = 50;
|
||||
#else // DECODE_AC
|
||||
// Suits most messages, while not swallowing many repeats.
|
||||
const uint8_t kTimeout = 15;
|
||||
#endif // DECODE_AC
|
||||
|
||||
const uint16_t kMinUnknownSize = 12;
|
||||
const uint8_t kTolerancePercentage = kTolerance; // kTolerance is normally 25%
|
||||
|
||||
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
|
||||
decode_results results;
|
||||
|
||||
void setup()
|
||||
{
|
||||
M5StackChan.begin();
|
||||
Serial.begin(115200);
|
||||
|
||||
// Perform a low level sanity checks that the compiler performs bit field
|
||||
// packing as we expect and Endianness is as we expect.
|
||||
assert(irutils::lowLevelSanityCheck() == 0);
|
||||
|
||||
#if DECODE_HASH
|
||||
// Ignore messages with less than minimum on or off pulses.
|
||||
irrecv.setUnknownThreshold(kMinUnknownSize);
|
||||
#endif // DECODE_HASH
|
||||
irrecv.setTolerance(kTolerancePercentage); // Override the default tolerance.
|
||||
irrecv.enableIRIn(); // Start the receiver
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// Check if the IR code has been received.
|
||||
if (irrecv.decode(&results)) {
|
||||
// Display a crude timestamp.
|
||||
uint32_t now = millis();
|
||||
Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
|
||||
// Check if we got an IR message that was to big for our capture buffer.
|
||||
if (results.overflow) Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
|
||||
// Display the library version the message was captured with.
|
||||
Serial.println(D_STR_LIBRARY " : v" _IRREMOTEESP8266_VERSION_STR "\n");
|
||||
// Display the tolerance percentage if it has been change from the default.
|
||||
if (kTolerancePercentage != kTolerance) Serial.printf(D_STR_TOLERANCE " : %d%%\n", kTolerancePercentage);
|
||||
// Display the basic output of what we found.
|
||||
Serial.print(resultToHumanReadableBasic(&results));
|
||||
// Display any extra A/C info if we have it.
|
||||
String description = IRAcUtils::resultAcToString(&results);
|
||||
if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
|
||||
yield(); // Feed the WDT as the text output can take a while to print.
|
||||
// Output the results as source code
|
||||
Serial.println(resultToSourceCode(&results));
|
||||
Serial.println(); // Blank line between entries
|
||||
yield(); // Feed the WDT (again)
|
||||
}
|
||||
delay(50);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// More examples on: https://github.com/crankyoldgit/IRremoteESP8266/tree/master/examples
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
#include <IRremoteESP8266.h>
|
||||
#include <IRsend.h>
|
||||
|
||||
const uint16_t kIrLed = 5; // IR tx pin
|
||||
|
||||
IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message.
|
||||
|
||||
// Example of data captured by IRrecvDumpV2.ino
|
||||
uint16_t rawData[67] = {9000, 4500, 650, 550, 650, 1650, 600, 550, 650, 550, 600, 1650, 650, 550,
|
||||
600, 1650, 650, 1650, 650, 1650, 600, 550, 650, 1650, 650, 1650, 650, 550,
|
||||
600, 1650, 650, 1650, 650, 550, 650, 550, 650, 1650, 650, 550, 650, 550,
|
||||
650, 550, 600, 550, 650, 550, 650, 550, 650, 1650, 600, 550, 650, 1650,
|
||||
650, 1650, 650, 1650, 650, 1650, 650, 1650, 650, 1650, 600};
|
||||
// Example Samsung A/C state captured from IRrecvDumpV2.ino
|
||||
uint8_t samsungState[kSamsungAcStateLength] = {0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0,
|
||||
0x01, 0xE2, 0xFE, 0x71, 0x40, 0x11, 0xF0};
|
||||
|
||||
void setup()
|
||||
{
|
||||
M5StackChan.begin();
|
||||
Serial.begin(115200);
|
||||
irsend.begin();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
Serial.println("NEC");
|
||||
irsend.sendNEC(0x00FFE01FUL);
|
||||
delay(2000);
|
||||
Serial.println("Sony");
|
||||
irsend.sendSony(0xa90, 12, 2); // 12 bits & 2 repeats
|
||||
delay(2000);
|
||||
Serial.println("a rawData capture from IRrecvDumpV2");
|
||||
irsend.sendRaw(rawData, 67, 38); // Send a raw data capture at 38kHz.
|
||||
delay(2000);
|
||||
Serial.println("a Samsung A/C state from IRrecvDumpV2");
|
||||
irsend.sendSamsungAC(samsungState);
|
||||
delay(2000);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// More examples on: https://github.com/m5stack/M5Unit-NFC/tree/main/examples
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
#include <M5UnitUnified.h>
|
||||
#include <M5UnitUnifiedNFC.h>
|
||||
#include <M5Utility.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace m5::nfc::a;
|
||||
|
||||
namespace {
|
||||
auto& lcd = M5.Display;
|
||||
m5::unit::UnitUnified Units;
|
||||
m5::unit::UnitNFC unit{}; // I2C
|
||||
m5::nfc::NFCLayerA nfc_a{unit};
|
||||
} // namespace
|
||||
|
||||
void setup()
|
||||
{
|
||||
M5StackChan.begin();
|
||||
|
||||
if (!Units.add(unit, M5.In_I2C) || !Units.begin()) {
|
||||
M5_LOGE("Failed to begin");
|
||||
lcd.clear(TFT_RED);
|
||||
while (true) {
|
||||
m5::utility::delay(10000);
|
||||
}
|
||||
}
|
||||
M5_LOGI("M5UnitUnified has been begun");
|
||||
M5_LOGI("%s", Units.debugInfo().c_str());
|
||||
|
||||
if (lcd.width() < lcd.height()) {
|
||||
lcd.setRotation(1);
|
||||
}
|
||||
lcd.setFont(&fonts::Font0);
|
||||
lcd.fillScreen(0);
|
||||
lcd.setCursor(0, 0);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
M5StackChan.update();
|
||||
Units.update();
|
||||
|
||||
std::vector<PICC> piccs;
|
||||
if (nfc_a.detect(piccs)) {
|
||||
lcd.fillScreen(0);
|
||||
lcd.setCursor(0, 0);
|
||||
uint16_t idx{};
|
||||
for (auto&& u : piccs) {
|
||||
M5.Speaker.tone(6000, 5);
|
||||
// detect only performs a provisional classification based on sak, so further identification is required
|
||||
if (nfc_a.identify(u)) {
|
||||
M5.Log.printf("PICC:%s %s %04X/%02X %u/%u\n", u.uidAsString().c_str(), u.typeAsString().c_str(), u.atqa,
|
||||
u.sak, u.userAreaSize(), u.totalSize());
|
||||
lcd.printf("[%2u]:PICC:<%s> %s\n", idx, u.uidAsString().c_str(), u.typeAsString().c_str());
|
||||
++idx;
|
||||
} else {
|
||||
M5_LOGW("Failed to identify %s %s %04X/%02X %u/%u", u.uidAsString().c_str(), u.typeAsString().c_str(),
|
||||
u.atqa, u.sak, u.userAreaSize(), u.totalSize());
|
||||
}
|
||||
}
|
||||
if (idx) {
|
||||
M5.Speaker.tone(3000, 10);
|
||||
lcd.printf("==> %u PICC\n", idx);
|
||||
M5.Log.printf("==> %u PICC\n", idx);
|
||||
}
|
||||
nfc_a.deactivate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// More examples on: https://github.com/m5stack/M5Unit-NFC/tree/main/examples
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
#include <M5UnitUnified.h>
|
||||
#include <M5UnitUnifiedNFC.h>
|
||||
#include <M5Utility.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace m5::nfc;
|
||||
using namespace m5::nfc::a;
|
||||
using namespace m5::nfc::a::mifare;
|
||||
using namespace m5::nfc::a::mifare::classic;
|
||||
|
||||
namespace {
|
||||
auto& lcd = M5.Display;
|
||||
m5::unit::UnitUnified Units;
|
||||
m5::unit::UnitNFC unit{}; // I2C
|
||||
m5::nfc::EmulationLayerA emu_a{unit};
|
||||
|
||||
PICC picc{};
|
||||
|
||||
constexpr Type type{Type::MIFARE_Ultralight};
|
||||
constexpr uint8_t uid[] = {0x04, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE};
|
||||
uint8_t picc_memory[] = {
|
||||
0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0x00, 0x00, 0x00, //
|
||||
0x00, 0xA3, 0x00, 0x00, //
|
||||
0xE1, 0x10, 0x06, 0x00, //
|
||||
0x03, 0x25, 0x91, 0x01, //
|
||||
0x0D, 0x55, 0x04, 0x6D, //
|
||||
0x35, 0x73, 0x74, 0x61, //
|
||||
0x63, 0x6B, 0x2E, 0x63, //
|
||||
0x6F, 0x6D, 0x2F, 0x51, //
|
||||
0x01, 0x10, 0x54, 0x02, //
|
||||
0x65, 0x6E, 0x48, 0x65, //
|
||||
0x6C, 0x6C, 0x6F, 0x20, //
|
||||
0x4D, 0x35, 0x53, 0x74, //
|
||||
0x61, 0x63, 0x6B, 0xFE, //
|
||||
0x44, 0x45, 0x46, 0x00, //
|
||||
0x44, 0x45, 0x46, 0x00, //
|
||||
};
|
||||
|
||||
uint8_t bcc8(const uint8_t* p, const uint8_t len, const uint8_t init = 0)
|
||||
{
|
||||
uint8_t v = init;
|
||||
for (uint_fast8_t i = 0; i < len; ++i) {
|
||||
v ^= p[i];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Correctly embed the Ultralight and NTAG UIDs into memory
|
||||
void embed_uid(uint8_t mem[9], const uint8_t uid[7])
|
||||
{
|
||||
memcpy(mem, uid, 3);
|
||||
mem[3] = bcc8(uid, 3, 0x88 /* CT */);
|
||||
memcpy(mem + 4, uid + 3, 4);
|
||||
mem[8] = bcc8(uid + 3, 4);
|
||||
}
|
||||
|
||||
constexpr uint16_t color_table[] = {
|
||||
// None, Off, Idle, Ready, Active, Halt };
|
||||
TFT_BLACK, TFT_RED, TFT_BLUE, TFT_YELLOW, TFT_GREEN, TFT_MAGENTA};
|
||||
constexpr const char* state_table[] = {"-", "O", "I", "R", "A", "H"};
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup()
|
||||
{
|
||||
M5StackChan.begin();
|
||||
|
||||
// Emulation settings
|
||||
auto cfg = unit.config();
|
||||
cfg.emulation = true;
|
||||
cfg.mode = NFC::A;
|
||||
unit.config(cfg);
|
||||
|
||||
if (!Units.add(unit, M5.In_I2C) || !Units.begin()) {
|
||||
M5_LOGE("Failed to begin");
|
||||
lcd.clear(TFT_RED);
|
||||
while (true) {
|
||||
m5::utility::delay(10000);
|
||||
}
|
||||
}
|
||||
M5_LOGI("M5UnitUnified has been begun");
|
||||
M5_LOGI("%s", Units.debugInfo().c_str());
|
||||
|
||||
if (lcd.width() < lcd.height()) {
|
||||
lcd.setRotation(1);
|
||||
}
|
||||
lcd.setFont(&fonts::Font2);
|
||||
|
||||
//
|
||||
lcd.startWrite();
|
||||
lcd.fillScreen(TFT_RED);
|
||||
if (picc.emulate(type, uid, sizeof(uid))) {
|
||||
embed_uid(picc_memory, uid);
|
||||
if (emu_a.begin(picc, picc_memory, sizeof(picc_memory))) {
|
||||
lcd.fillScreen(TFT_DARKGREEN);
|
||||
lcd.setCursor(0, 16);
|
||||
const auto& e_picc = emu_a.emulatePICC();
|
||||
M5.Log.printf("Emulation:%s %s ATQA:%04X SAK:%u\n", e_picc.typeAsString().c_str(),
|
||||
e_picc.uidAsString().c_str(), e_picc.atqa, e_picc.sak);
|
||||
lcd.printf("%s\n%s\nATQA:%04X SAK:%u", e_picc.typeAsString().c_str(), e_picc.uidAsString().c_str(),
|
||||
e_picc.atqa, e_picc.sak);
|
||||
}
|
||||
}
|
||||
lcd.fillRect(0, 0, 32, 16, color_table[0]);
|
||||
lcd.drawString(state_table[0], 0, 0);
|
||||
lcd.endWrite();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
M5StackChan.update();
|
||||
Units.update();
|
||||
emu_a.update(); // Need call in loop
|
||||
|
||||
static EmulationLayerA::State latest{};
|
||||
auto state = emu_a.state();
|
||||
if (latest != state) {
|
||||
latest = state;
|
||||
lcd.startWrite();
|
||||
lcd.fillRect(0, 0, 32, 16, color_table[m5::stl::to_underlying(state)]);
|
||||
lcd.drawString(state_table[m5::stl::to_underlying(state)], 0, 0);
|
||||
lcd.endWrite();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
struct Color_t {
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
};
|
||||
|
||||
static std::vector<Color_t> colors = {
|
||||
{0, 0, 0}, {168, 0, 0}, {0, 168, 0}, {0, 0, 168}, {168, 168, 0}, {168, 0, 168}, {0, 168, 168}, {168, 168, 168},
|
||||
};
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
/* There are 12 RGB LEDs, index 0-5 are on the left, 6-11 are on the right */
|
||||
for (int color_index = 0; color_index < colors.size(); color_index++) {
|
||||
for (int led_index = 0; led_index < 12; led_index++) {
|
||||
M5StackChan.setRgbColor(led_index, colors[color_index].r, colors[color_index].g, colors[color_index].b);
|
||||
M5StackChan.refreshRgb();
|
||||
delay(1000 / 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
|
||||
/* Setup display */
|
||||
M5StackChan.Display().setTextSize(2);
|
||||
M5StackChan.Display().setTextScroll(true);
|
||||
M5StackChan.Display().setTextColor(TFT_YELLOW);
|
||||
M5StackChan.Display().printf("> Touch the top to start\n");
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
|
||||
// Set to false if high-frequency updates are needed
|
||||
// M5StackChan.Motion.setAutoAngleSyncEnabled(false);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
M5StackChan.update();
|
||||
if (M5StackChan.TouchSensor.wasPressed()) {
|
||||
delay(200);
|
||||
|
||||
/* Angle unit: 10 = 1 degrees, Speed range: 0~1000 */
|
||||
/* Range X: -1280 ~ 1280 (-128° ~ 128°), Range Y: 0 ~ 900 (0° ~ 90°) */
|
||||
|
||||
/* Move to home position (0, 0) */
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to 100° */
|
||||
M5StackChan.Motion.moveX(1000, 200);
|
||||
M5StackChan.Display().printf("> Turn Left (Slow: 200)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to -100° */
|
||||
M5StackChan.Motion.moveX(-1000, 200);
|
||||
M5StackChan.Display().printf("> Turn Right (Slow: 200)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to 100° */
|
||||
M5StackChan.Motion.moveX(1000, 800);
|
||||
M5StackChan.Display().printf("> Turn Left (Fast: 800)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to -100° */
|
||||
M5StackChan.Motion.moveX(-1000, 800);
|
||||
M5StackChan.Display().printf("> Turn Right (Fast: 800)\n");
|
||||
delay(2000);
|
||||
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move Y servo to 90° */
|
||||
M5StackChan.Motion.moveY(900, 200);
|
||||
M5StackChan.Display().printf("> Look Up (Slow: 200)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move Y servo to 0° */
|
||||
M5StackChan.Motion.moveY(0, 200);
|
||||
M5StackChan.Display().printf("> Look Down (Slow: 200)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move Y servo to 90° */
|
||||
M5StackChan.Motion.moveY(900, 800);
|
||||
M5StackChan.Display().printf("> Look Up (Fast: 800)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move Y servo to 0° */
|
||||
M5StackChan.Motion.moveY(0, 800);
|
||||
M5StackChan.Display().printf("> Look Down (Fast: 800)\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to 60°, Y servo to 70° */
|
||||
M5StackChan.Motion.move(600, 700);
|
||||
M5StackChan.Display().printf("> Top Left\n");
|
||||
delay(2000);
|
||||
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
/* Move X servo to -60°, Y servo to 70° */
|
||||
M5StackChan.Motion.move(-600, 700);
|
||||
M5StackChan.Display().printf("> Top Right\n");
|
||||
delay(2000);
|
||||
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
/* Only X axis supports continuous 360° rotation. Y axis does not. */
|
||||
/* Velocity range: -1000 ~ 1000 (Negative: CW, Positive: CCW) */
|
||||
|
||||
/* Rotate clockwise */
|
||||
M5StackChan.Motion.rotateX(-300);
|
||||
M5StackChan.Display().printf("> Rotate clockwise (Slow: 300)\n");
|
||||
delay(3000);
|
||||
|
||||
/* Rotate clockwise */
|
||||
M5StackChan.Motion.rotateX(-800);
|
||||
M5StackChan.Display().printf("> Rotate clockwise (Fast: 800)\n");
|
||||
delay(3000);
|
||||
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
/* Rotate counter-clockwise */
|
||||
M5StackChan.Motion.rotateX(300);
|
||||
M5StackChan.Display().printf("> Rotate counter-clockwise (Slow: 300)\n");
|
||||
delay(3000);
|
||||
|
||||
/* Rotate counter-clockwise */
|
||||
M5StackChan.Motion.rotateX(800);
|
||||
M5StackChan.Display().printf("> Rotate counter-clockwise (Fast: 800)\n");
|
||||
delay(3000);
|
||||
|
||||
M5StackChan.Motion.goHome();
|
||||
M5StackChan.Display().printf("> Go home\n");
|
||||
delay(2000);
|
||||
|
||||
M5StackChan.Display().setTextColor(TFT_YELLOW);
|
||||
M5StackChan.Display().printf("> Touch the top to start\n");
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Angle unit: 10 = 1 degrees, Speed range: 0~1000
|
||||
* Range X: -1280 ~ 1280 (-128° ~ 128°), Range Y: 0 ~ 900 (0° ~ 90°)
|
||||
*
|
||||
*/
|
||||
struct Keyframe_t {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int speed = 500;
|
||||
uint32_t interval = 0;
|
||||
};
|
||||
|
||||
/* Move around */
|
||||
std::vector<Keyframe_t> dance_1 = {
|
||||
{0, 0, 500, 1000}, // Home, 1s
|
||||
{600, 200, 800, 500}, // Left, fast
|
||||
{-600, 200, 800, 500}, // Right, fast
|
||||
{600, 200, 800, 500}, // Left, fast
|
||||
{-600, 200, 800, 500}, // Right, fast
|
||||
{0, 800, 900, 400}, // Look Up
|
||||
{0, 0, 900, 400}, // Look Down
|
||||
{0, 800, 900, 400}, // Look Up
|
||||
{0, 0, 900, 400}, // Look Down
|
||||
{800, 700, 700, 500}, // Top Left
|
||||
{-800, 700, 700, 500}, // Top Right
|
||||
{800, 700, 700, 500}, // Top Left
|
||||
{-800, 700, 700, 500}, // Top Right
|
||||
{0, 0, 500, 1000} // Back Home
|
||||
};
|
||||
|
||||
/* Shake */
|
||||
std::vector<Keyframe_t> dance_2 = {
|
||||
{0, 0, 500, 1000}, // Home
|
||||
{300, 0, 750, 250}, // Shake Left
|
||||
{-300, 0, 750, 250}, // Shake Right
|
||||
{300, 0, 750, 250}, // Shake Left
|
||||
{-300, 0, 750, 250}, // Shake Right
|
||||
{300, 0, 750, 250}, // Shake Left
|
||||
{-300, 0, 750, 250}, // Shake Right
|
||||
{0, 0, 500, 1000} // Back Home
|
||||
};
|
||||
|
||||
/* Nod */
|
||||
std::vector<Keyframe_t> dance_3 = {
|
||||
{0, 0, 500, 1000}, // Home
|
||||
{0, 350, 900, 250}, // Nod Up
|
||||
{0, 0, 900, 250}, // Nod Down
|
||||
{0, 350, 900, 250}, // Nod Up
|
||||
{0, 0, 900, 250}, // Nod Down
|
||||
{0, 350, 900, 250}, // Nod Up
|
||||
{0, 0, 900, 250}, // Nod Down
|
||||
{0, 0, 500, 500} // Back Home
|
||||
};
|
||||
|
||||
std::vector<std::vector<Keyframe_t>*> dances = {
|
||||
&dance_1,
|
||||
&dance_2,
|
||||
&dance_3,
|
||||
};
|
||||
|
||||
static int dance_index = 0;
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
|
||||
/* Setup display */
|
||||
M5StackChan.Display().setTextSize(2);
|
||||
M5StackChan.Display().setTextScroll(true);
|
||||
M5StackChan.Display().setTextColor(TFT_YELLOW);
|
||||
M5StackChan.Display().printf("> Touch the top to start\n");
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
|
||||
// Disable auto angle sync for smooth and continuous movement
|
||||
M5StackChan.Motion.setAutoAngleSyncEnabled(false);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
M5StackChan.update();
|
||||
if (M5StackChan.TouchSensor.wasPressed()) {
|
||||
delay(200);
|
||||
|
||||
/* Perform dance */
|
||||
M5StackChan.Display().printf("> Dance #%d!\n", dance_index + 1);
|
||||
for (const auto& frame : *dances[dance_index]) {
|
||||
M5StackChan.Motion.move(frame.x, frame.y, frame.speed);
|
||||
delay(frame.interval);
|
||||
}
|
||||
M5StackChan.Display().printf("> Finished!\n");
|
||||
|
||||
/* Next dance */
|
||||
dance_index = (dance_index + 1) % dances.size();
|
||||
|
||||
M5StackChan.Display().setTextColor(TFT_YELLOW);
|
||||
M5StackChan.Display().printf("> Touch the top to start\n");
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kBackgroundColor = TFT_BLACK;
|
||||
constexpr uint16_t kBorderColor = TFT_WHITE;
|
||||
constexpr uint16_t kTopButtonColor = 0x39C7;
|
||||
constexpr uint16_t kTopButtonPressedColor = 0x2204;
|
||||
constexpr uint16_t kBottomButtonColor = 0x03EF;
|
||||
constexpr uint16_t kBottomButtonPressedColor = 0x01E8;
|
||||
constexpr uint16_t kTextColor = TFT_WHITE;
|
||||
|
||||
enum class ButtonZone {
|
||||
None,
|
||||
SetHome,
|
||||
GoHome,
|
||||
};
|
||||
|
||||
ButtonZone pressed_zone = ButtonZone::None;
|
||||
|
||||
ButtonZone getButtonZone(const int16_t y, const int16_t height)
|
||||
{
|
||||
return y < (height / 2) ? ButtonZone::SetHome : ButtonZone::GoHome;
|
||||
}
|
||||
|
||||
void drawButton(const int16_t x, const int16_t y, const int16_t w, const int16_t h, const uint16_t color,
|
||||
const char* line_1, const char* line_2)
|
||||
{
|
||||
auto& display = M5StackChan.Display();
|
||||
|
||||
display.fillRect(x, y, w, h, color);
|
||||
display.drawRect(x, y, w, h, kBorderColor);
|
||||
|
||||
display.setTextDatum(middle_center);
|
||||
display.setTextColor(kTextColor, color);
|
||||
display.setTextSize(2);
|
||||
display.drawString(line_1, x + w / 2, y + h / 2 - 12);
|
||||
display.drawString(line_2, x + w / 2, y + h / 2 + 12);
|
||||
}
|
||||
|
||||
void drawUi(ButtonZone active_zone)
|
||||
{
|
||||
auto& display = M5StackChan.Display();
|
||||
const int16_t width = display.width();
|
||||
const int16_t height = display.height();
|
||||
const int16_t gap = 8;
|
||||
const int16_t button_x = 8;
|
||||
const int16_t button_w = width - button_x * 2;
|
||||
const int16_t half_h = (height - gap) / 2;
|
||||
|
||||
display.startWrite();
|
||||
display.fillScreen(kBackgroundColor);
|
||||
display.fillRect(0, half_h, width, gap, kBackgroundColor);
|
||||
|
||||
drawButton(button_x, 8, button_w, half_h - 12,
|
||||
active_zone == ButtonZone::SetHome ? kTopButtonPressedColor : kTopButtonColor, "set current postion",
|
||||
"as home");
|
||||
|
||||
drawButton(button_x, half_h + gap + 4, button_w, height - (half_h + gap + 12),
|
||||
active_zone == ButtonZone::GoHome ? kBottomButtonPressedColor : kBottomButtonColor, "move to", "home");
|
||||
|
||||
display.endWrite();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
|
||||
/* Setup display */
|
||||
M5StackChan.Display().setTextScroll(false);
|
||||
drawUi(ButtonZone::None);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
M5StackChan.update();
|
||||
auto& display = M5StackChan.Display();
|
||||
const int16_t screen_height = display.height();
|
||||
int16_t touch_x = 0;
|
||||
int16_t touch_y = 0;
|
||||
const bool touching = display.getTouch(&touch_x, &touch_y);
|
||||
|
||||
if (touching) {
|
||||
const ButtonZone current_zone = getButtonZone(touch_y, screen_height);
|
||||
if (current_zone != pressed_zone) {
|
||||
pressed_zone = current_zone;
|
||||
drawUi(pressed_zone);
|
||||
}
|
||||
} else if (pressed_zone != ButtonZone::None) {
|
||||
const ButtonZone released_zone = pressed_zone;
|
||||
pressed_zone = ButtonZone::None;
|
||||
drawUi(ButtonZone::None);
|
||||
|
||||
if (released_zone == ButtonZone::SetHome) {
|
||||
M5StackChan.Motion.setCurrentPostionAsHome();
|
||||
} else if (released_zone == ButtonZone::GoHome) {
|
||||
M5StackChan.Motion.goHome();
|
||||
}
|
||||
}
|
||||
|
||||
delay(16);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include <M5StackChan.h>
|
||||
|
||||
void setup()
|
||||
{
|
||||
/* Init StackChan */
|
||||
M5StackChan.begin();
|
||||
|
||||
/* Setup display */
|
||||
M5StackChan.Display().setTextSize(2);
|
||||
M5StackChan.Display().setTextScroll(true);
|
||||
M5StackChan.Display().setTextColor(TFT_YELLOW);
|
||||
M5StackChan.Display().printf("> Touch or swipe the top\n");
|
||||
M5StackChan.Display().setTextColor(TFT_GREENYELLOW);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
/* Update touch sensor */
|
||||
M5StackChan.update();
|
||||
|
||||
auto& ts = M5StackChan.TouchSensor;
|
||||
|
||||
if (ts.wasClicked()) {
|
||||
M5StackChan.Display().printf("> Was clicked\n");
|
||||
}
|
||||
|
||||
if (ts.wasSwipedForward()) {
|
||||
M5StackChan.Display().printf("> Was swiped forward\n");
|
||||
}
|
||||
|
||||
if (ts.wasSwipedBackward()) {
|
||||
M5StackChan.Display().printf("> Was swiped backward\n");
|
||||
}
|
||||
|
||||
delay(50);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "M5StackChan",
|
||||
"description": "Library for M5StackChan",
|
||||
"keywords": "M5Stack,M5StackChan",
|
||||
"authors": {
|
||||
"name": "M5Stack"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/M5Stack/M5StackChan.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"M5Unified": "*",
|
||||
"M5GFX": "*",
|
||||
"IRremoteESP8266": "*",
|
||||
"M5Unit-NFC": "*"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"frameworks": "arduino",
|
||||
"platforms": "espressif32"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
name=M5StackChan
|
||||
version=1.0.0
|
||||
author=M5Stack
|
||||
maintainer=M5Stack
|
||||
sentence=M5StackChan is a library for M5StackChan
|
||||
paragraph=
|
||||
category=Device Control
|
||||
url=https://github.com/m5stack/M5StackChan.git
|
||||
architectures=esp32
|
||||
includes=src,src/utils
|
||||
depends=M5Unified,IRremoteESP8266,M5Unit-NFC
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "M5StackChan.h"
|
||||
#include "drivers/PY32IOExpander/PY32IOExpander.hpp"
|
||||
#include "drivers/SCServo_lib/src/SCSCL.h"
|
||||
#include "utils/compat/make_unique.h"
|
||||
#include "utils/settings/settings.h"
|
||||
#include "utility/power/INA226_Class.hpp"
|
||||
#include <esp_log.h>
|
||||
|
||||
using namespace m5;
|
||||
|
||||
M5StackChan_Class M5StackChan;
|
||||
|
||||
static const char* TAG = "M5StackChan";
|
||||
|
||||
void M5StackChan_Class::begin()
|
||||
{
|
||||
M5.begin();
|
||||
TouchSensor.begin();
|
||||
io_expander_init();
|
||||
servo_init();
|
||||
ina226_init();
|
||||
}
|
||||
|
||||
void M5StackChan_Class::update()
|
||||
{
|
||||
M5.update();
|
||||
TouchSensor.update();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* IO Expander */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
std::unique_ptr<PY32IOExpander_Class> _io_expander;
|
||||
|
||||
void M5StackChan_Class::io_expander_init()
|
||||
{
|
||||
_io_expander = std::make_unique<m5::PY32IOExpander_Class>();
|
||||
|
||||
// PY32 IO Expander may boot slowly, wait for it
|
||||
uint32_t start_tick = millis();
|
||||
while (1) {
|
||||
delay(200);
|
||||
|
||||
if (millis() - start_tick > 1200) {
|
||||
ESP_LOGE(TAG, "IO expander init timeout");
|
||||
_io_expander.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
if (_io_expander->begin()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_io_expander) {
|
||||
// VM EN
|
||||
_io_expander->setDirection(0, true); // Output
|
||||
_io_expander->setPullMode(0, true); // Pull-up
|
||||
setServoPowerEnabled(true);
|
||||
delay(200);
|
||||
|
||||
// RGB
|
||||
_io_expander->setDirection(13, true); // Output
|
||||
_io_expander->setPullMode(13, true); // Pull-up
|
||||
_io_expander->setDriveMode(13, false); // Push-pull
|
||||
_io_expander->setLedCount(12);
|
||||
delay(200);
|
||||
showRgbColor(0, 0, 0);
|
||||
delay(50);
|
||||
showRgbColor(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void M5StackChan_Class::setServoPowerEnabled(bool enabled)
|
||||
{
|
||||
if (!_io_expander) {
|
||||
return;
|
||||
}
|
||||
_io_expander->digitalWrite(0, enabled ? true : false);
|
||||
}
|
||||
|
||||
void M5StackChan_Class::setRgbColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
if (!_io_expander) {
|
||||
return;
|
||||
}
|
||||
_io_expander->setLedColor(index, r, g, b);
|
||||
}
|
||||
|
||||
void M5StackChan_Class::refreshRgb()
|
||||
{
|
||||
if (!_io_expander) {
|
||||
return;
|
||||
}
|
||||
_io_expander->refreshLeds();
|
||||
}
|
||||
|
||||
void M5StackChan_Class::showRgbColor(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
for (int i = 0; i < 12; i++) {
|
||||
setRgbColor(i, r, g, b);
|
||||
}
|
||||
refreshRgb();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Servo */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
static SCSCL _scs_bus;
|
||||
|
||||
struct ServoConfig_t {
|
||||
int id = -1;
|
||||
int defaultZeroPos = 0;
|
||||
uitk_intl::Vector2i angleLimit;
|
||||
uitk_intl::Vector2i rawPosLimit;
|
||||
std::string settingNs;
|
||||
std::string settingZeroPositionKey;
|
||||
bool enablePwmMode = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Servo class implement
|
||||
*
|
||||
*/
|
||||
class ScsServo : public stackchan::motion::Servo {
|
||||
public:
|
||||
static inline const std::string _tag = "ScsServo";
|
||||
|
||||
ScsServo(const ServoConfig_t& config) : _config(config)
|
||||
{
|
||||
}
|
||||
|
||||
void init() override
|
||||
{
|
||||
set_angle_limit(_config.angleLimit);
|
||||
get_zero_pos_from_nvs();
|
||||
Servo::init();
|
||||
}
|
||||
|
||||
void get_zero_pos_from_nvs()
|
||||
{
|
||||
_zero_pos = _config.defaultZeroPos;
|
||||
bool is_valid = false;
|
||||
|
||||
{
|
||||
Settings settings(_config.settingNs, false);
|
||||
int nvs_zero_pos = settings.GetInt(_config.settingZeroPositionKey, -1);
|
||||
|
||||
// Limit check
|
||||
if (nvs_zero_pos >= _config.rawPosLimit.x && nvs_zero_pos <= _config.rawPosLimit.y) {
|
||||
_zero_pos = nvs_zero_pos;
|
||||
is_valid = true;
|
||||
ESP_LOGI(TAG, "Servo ID: %d get zero pos: %d from settings", _config.id, _zero_pos);
|
||||
} else {
|
||||
is_valid = false;
|
||||
ESP_LOGW(TAG, "Servo ID: %d get invalid zero pos: %d from settings", _config.id, nvs_zero_pos);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_valid) {
|
||||
_zero_pos = _config.defaultZeroPos;
|
||||
ESP_LOGI(TAG, "Servo ID: %d override zero pos to default: %d", _config.id, _zero_pos);
|
||||
|
||||
Settings settings(_config.settingNs, true);
|
||||
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void set_angle_impl(int angle) override
|
||||
{
|
||||
int mapped_angle = _zero_pos + angle * 16 / 5 / 10; // 一步对应 0.3125度, 0.3125 = 5/16
|
||||
mapped_angle = uitk_intl::clamp(mapped_angle, _config.rawPosLimit.x, _config.rawPosLimit.y);
|
||||
|
||||
// ESP_LOGI(TAG, "Servo ID: %d mapped angle: %d", _config.id, mapped_angle);
|
||||
|
||||
check_mode(Mode::Position);
|
||||
_scs_bus.WritePos(_config.id, mapped_angle, 20, 0);
|
||||
}
|
||||
|
||||
int getCurrentAngle() override
|
||||
{
|
||||
int current_pos = _scs_bus.ReadPos(_config.id);
|
||||
int angle = (current_pos - _zero_pos) * 5 * 10 / 16;
|
||||
angle = uitk_intl::clamp(angle, getAngleLimit().x, getAngleLimit().y);
|
||||
// ESP_LOGI(TAG, "Servo ID: %d current pos: %d angle: %d", _id, current_pos, angle);
|
||||
return angle;
|
||||
}
|
||||
|
||||
bool is_moving_impl() override
|
||||
{
|
||||
int moving = _scs_bus.ReadMove(_config.id);
|
||||
// ESP_LOGI(TAG, "Servo ID: %d moving: %d", _id, moving);
|
||||
return moving != 0;
|
||||
}
|
||||
|
||||
void setTorqueEnabled(bool enabled) override
|
||||
{
|
||||
Servo::setTorqueEnabled(enabled);
|
||||
_scs_bus.EnableTorque(_config.id, enabled ? 1 : 0);
|
||||
// ESP_LOGI(TAG, "Servo ID: %d set torque: %d", _id, enabled);
|
||||
}
|
||||
|
||||
bool getTorqueEnabled() override
|
||||
{
|
||||
int torque_enable = _scs_bus.ReadToqueEnable(_config.id);
|
||||
// ESP_LOGI(TAG, "Servo ID: %d torque enable: %d", _id, torque_enable);
|
||||
return torque_enable > 0;
|
||||
}
|
||||
|
||||
void setCurrentAngleAsZero() override
|
||||
{
|
||||
_zero_pos = _scs_bus.ReadPos(_config.id);
|
||||
|
||||
Settings settings(_config.settingNs, true);
|
||||
settings.SetInt(_config.settingZeroPositionKey, _zero_pos);
|
||||
|
||||
ESP_LOGI(TAG, "Servo ID: %d set zero pos: %d to settings", _config.id, _zero_pos);
|
||||
}
|
||||
|
||||
void rotate(int velocity) override
|
||||
{
|
||||
velocity = uitk_intl::clamp(velocity, -1000, 1000);
|
||||
|
||||
if (!_config.enablePwmMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
int mapped_velocity = uitk_intl::map_range(velocity, 0, 1000, 0, 1023);
|
||||
|
||||
check_mode(Mode::PWM);
|
||||
_scs_bus.WritePWM(_config.id, mapped_velocity);
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Mode { Position = 0, PWM = 1 };
|
||||
|
||||
ServoConfig_t _config;
|
||||
int _zero_pos = 0;
|
||||
Mode _current_mode = Mode::Position;
|
||||
|
||||
void check_mode(Mode targetMode)
|
||||
{
|
||||
if (targetMode == _current_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scs_bus.SwitchMode(_config.id, static_cast<uint8_t>(targetMode));
|
||||
_current_mode = targetMode;
|
||||
}
|
||||
};
|
||||
|
||||
void M5StackChan_Class::servo_init()
|
||||
{
|
||||
_scs_bus.begin(UART_NUM_1, 1000000, 6, 7);
|
||||
|
||||
uitk_intl::ui_hal::on_delay([](uint32_t ms) { delay(ms); });
|
||||
uitk_intl::ui_hal::on_get_tick([]() { return millis(); });
|
||||
|
||||
ServoConfig_t yaw_servo_config;
|
||||
yaw_servo_config.id = 1;
|
||||
yaw_servo_config.defaultZeroPos = 460;
|
||||
yaw_servo_config.angleLimit = uitk_intl::Vector2i(-1280, 1280);
|
||||
yaw_servo_config.rawPosLimit = uitk_intl::Vector2i(0, 1000);
|
||||
yaw_servo_config.settingNs = "servo";
|
||||
yaw_servo_config.settingZeroPositionKey = "zero_pos_1";
|
||||
yaw_servo_config.enablePwmMode = true;
|
||||
|
||||
ServoConfig_t pitch_servo_config;
|
||||
pitch_servo_config.id = 2;
|
||||
pitch_servo_config.defaultZeroPos = 620;
|
||||
pitch_servo_config.angleLimit = uitk_intl::Vector2i(0, 900);
|
||||
pitch_servo_config.rawPosLimit = uitk_intl::Vector2i(0, 1000);
|
||||
pitch_servo_config.settingNs = "servo";
|
||||
pitch_servo_config.settingZeroPositionKey = "zero_pos_2";
|
||||
|
||||
auto yaw_servo = std::make_unique<ScsServo>(yaw_servo_config);
|
||||
auto pitch_servo = std::make_unique<ScsServo>(pitch_servo_config);
|
||||
|
||||
Motion.init(std::move(yaw_servo), std::move(pitch_servo));
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* INA226 */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
std::unique_ptr<m5::INA226_Class> _ina226;
|
||||
|
||||
void M5StackChan_Class::ina226_init()
|
||||
{
|
||||
_ina226 = std::make_unique<m5::INA226_Class>(0x41);
|
||||
|
||||
m5::INA226_Class::config_t config;
|
||||
config.shunt_res = 0.01;
|
||||
config.max_expected_current = 8.19;
|
||||
_ina226->config(config);
|
||||
|
||||
if (!_ina226->begin()) {
|
||||
ESP_LOGE(TAG, "INA226 init failed");
|
||||
_ina226.reset();
|
||||
}
|
||||
}
|
||||
|
||||
float M5StackChan_Class::getBatteryVoltage()
|
||||
{
|
||||
float result = 0.0f;
|
||||
if (_ina226) {
|
||||
result = _ina226->getBusVoltage();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
float M5StackChan_Class::getBatteryCurrent()
|
||||
{
|
||||
float result = 0.0f;
|
||||
if (_ina226) {
|
||||
result = _ina226->getShuntCurrent();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#pragma once
|
||||
#include "utils/touch_sensor/touch_sensor.h"
|
||||
#include "utils/motion/motion.h"
|
||||
#include <M5GFX.h>
|
||||
#include <M5Unified.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace m5 {
|
||||
|
||||
class M5StackChan_Class {
|
||||
public:
|
||||
void begin();
|
||||
void update();
|
||||
|
||||
inline LGFX_Device& Display()
|
||||
{
|
||||
return M5.Display;
|
||||
}
|
||||
inline LGFX_Device& Lcd()
|
||||
{
|
||||
return M5.Lcd;
|
||||
}
|
||||
TouchSensor_Class TouchSensor;
|
||||
stackchan::motion::Motion Motion;
|
||||
|
||||
/**
|
||||
* @brief Enable or disable servo power.
|
||||
*
|
||||
* @param enabled
|
||||
*/
|
||||
void setServoPowerEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Set the Rgb Color object.
|
||||
* There are 12 RGB LEDs, 0-5 are on the left, 6-11 are on the right.
|
||||
*
|
||||
* @param index
|
||||
* @param r
|
||||
* @param g
|
||||
* @param b
|
||||
*/
|
||||
void setRgbColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/**
|
||||
* @brief Update the RGB LEDs with the set colors.
|
||||
*
|
||||
*/
|
||||
void refreshRgb();
|
||||
|
||||
/**
|
||||
* @brief Set all RGB LEDs to the specified color.
|
||||
*
|
||||
* @param r
|
||||
* @param g
|
||||
* @param b
|
||||
*/
|
||||
void showRgbColor(uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/**
|
||||
* @brief Get battery voltage.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
float getBatteryVoltage();
|
||||
|
||||
/**
|
||||
* @brief Get battery current.
|
||||
* Positive when discharging, negative when charging.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
float getBatteryCurrent();
|
||||
|
||||
protected:
|
||||
void io_expander_init();
|
||||
void servo_init();
|
||||
void ina226_init();
|
||||
};
|
||||
|
||||
} // namespace m5
|
||||
|
||||
extern m5::M5StackChan_Class M5StackChan;
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "PY32IOExpander.hpp"
|
||||
|
||||
namespace m5 {
|
||||
// Register definitions
|
||||
static constexpr uint8_t REG_UID_L = 0x00;
|
||||
static constexpr uint8_t REG_UID_H = 0x01;
|
||||
static constexpr uint8_t REG_VERSION = 0x02;
|
||||
static constexpr uint8_t REG_GPIO_M_L = 0x03;
|
||||
static constexpr uint8_t REG_GPIO_M_H = 0x04;
|
||||
static constexpr uint8_t REG_GPIO_O_L = 0x05;
|
||||
static constexpr uint8_t REG_GPIO_O_H = 0x06;
|
||||
static constexpr uint8_t REG_GPIO_I_L = 0x07;
|
||||
static constexpr uint8_t REG_GPIO_I_H = 0x08;
|
||||
static constexpr uint8_t REG_GPIO_PU_L = 0x09;
|
||||
static constexpr uint8_t REG_GPIO_PU_H = 0x0A;
|
||||
static constexpr uint8_t REG_GPIO_PD_L = 0x0B;
|
||||
static constexpr uint8_t REG_GPIO_PD_H = 0x0C;
|
||||
static constexpr uint8_t REG_GPIO_IE_L = 0x0D;
|
||||
static constexpr uint8_t REG_GPIO_IE_H = 0x0E;
|
||||
static constexpr uint8_t REG_GPIO_IT_L = 0x0F;
|
||||
static constexpr uint8_t REG_GPIO_IT_H = 0x10;
|
||||
static constexpr uint8_t REG_GPIO_IS_L = 0x11;
|
||||
static constexpr uint8_t REG_GPIO_IS_H = 0x12;
|
||||
static constexpr uint8_t REG_GPIO_DRV_L = 0x13;
|
||||
static constexpr uint8_t REG_GPIO_DRV_H = 0x14;
|
||||
|
||||
static constexpr uint8_t REG_ADC_CTRL = 0x15;
|
||||
static constexpr uint8_t REG_ADC_D_L = 0x16;
|
||||
static constexpr uint8_t REG_ADC_D_H = 0x17;
|
||||
|
||||
static constexpr uint8_t REG_PWM_FREQ_L = 0x25;
|
||||
static constexpr uint8_t REG_PWM_FREQ_H = 0x26;
|
||||
|
||||
static constexpr uint8_t REG_LED_CFG = 0x24;
|
||||
static constexpr uint8_t REG_LED_RAM_START = 0x30;
|
||||
|
||||
// PWM Duty Registers
|
||||
static constexpr uint8_t REG_PWM1_DUTY_L = 0x1B;
|
||||
static constexpr uint8_t REG_PWM1_DUTY_H = 0x1C;
|
||||
static constexpr uint8_t REG_PWM2_DUTY_L = 0x1D;
|
||||
static constexpr uint8_t REG_PWM2_DUTY_H = 0x1E;
|
||||
static constexpr uint8_t REG_PWM3_DUTY_L = 0x1F;
|
||||
static constexpr uint8_t REG_PWM3_DUTY_H = 0x20;
|
||||
static constexpr uint8_t REG_PWM4_DUTY_L = 0x21;
|
||||
static constexpr uint8_t REG_PWM4_DUTY_H = 0x22;
|
||||
|
||||
void PY32IOExpander_Class::_writeBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin, bool value)
|
||||
{
|
||||
if (pin < 8) {
|
||||
if (value)
|
||||
bitOn(reg_l, 1 << pin);
|
||||
else
|
||||
bitOff(reg_l, 1 << pin);
|
||||
} else {
|
||||
if (value)
|
||||
bitOn(reg_h, 1 << (pin - 8));
|
||||
else
|
||||
bitOff(reg_h, 1 << (pin - 8));
|
||||
}
|
||||
}
|
||||
|
||||
bool PY32IOExpander_Class::_readBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin)
|
||||
{
|
||||
if (pin < 8) {
|
||||
return (readRegister8(reg_l) & (1 << pin)) != 0;
|
||||
} else {
|
||||
return (readRegister8(reg_h) & (1 << (pin - 8))) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool PY32IOExpander_Class::begin()
|
||||
{
|
||||
uint8_t version = readRegister8(REG_VERSION);
|
||||
if (version == 0 || version == 0xFF) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setDirection(uint8_t pin, bool direction)
|
||||
{
|
||||
// direction: false=input (0), true=output (1)
|
||||
_writeBit(REG_GPIO_M_L, REG_GPIO_M_H, pin, direction);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::enablePull(uint8_t pin, bool enablePull)
|
||||
{
|
||||
if (enablePull) {
|
||||
// Enable Pull Up by default if neither is set
|
||||
bool pu = _readBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin);
|
||||
bool pd = _readBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin);
|
||||
if (!pu && !pd) {
|
||||
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, true);
|
||||
}
|
||||
// If one is already set, leave it.
|
||||
} else {
|
||||
// Disable both
|
||||
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, false);
|
||||
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, false);
|
||||
}
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setPullMode(uint8_t pin, bool mode)
|
||||
{
|
||||
// mode: false=down, true=up
|
||||
if (mode) {
|
||||
// Pull Up
|
||||
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, false);
|
||||
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, true);
|
||||
} else {
|
||||
// Pull Down
|
||||
_writeBit(REG_GPIO_PU_L, REG_GPIO_PU_H, pin, false);
|
||||
_writeBit(REG_GPIO_PD_L, REG_GPIO_PD_H, pin, true);
|
||||
}
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setDriveMode(uint8_t pin, bool openDrain)
|
||||
{
|
||||
// openDrain: false=push-pull (0), true=open-drain (1)
|
||||
_writeBit(REG_GPIO_DRV_L, REG_GPIO_DRV_H, pin, openDrain);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setHighImpedance(uint8_t pin, bool enable)
|
||||
{
|
||||
if (enable) {
|
||||
// Input mode
|
||||
setDirection(pin, false);
|
||||
// Disable pulls
|
||||
enablePull(pin, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool PY32IOExpander_Class::getWriteValue(uint8_t pin)
|
||||
{
|
||||
return _readBit(REG_GPIO_O_L, REG_GPIO_O_H, pin);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::digitalWrite(uint8_t pin, bool level)
|
||||
{
|
||||
_writeBit(REG_GPIO_O_L, REG_GPIO_O_H, pin, level);
|
||||
}
|
||||
|
||||
bool PY32IOExpander_Class::digitalRead(uint8_t pin)
|
||||
{
|
||||
return _readBit(REG_GPIO_I_L, REG_GPIO_I_H, pin);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::resetIrq()
|
||||
{
|
||||
// Clear all interrupts by writing 1s to IS registers
|
||||
writeRegister8(REG_GPIO_IS_L, 0xFF);
|
||||
writeRegister8(REG_GPIO_IS_H, 0xFF); // Only bits 0-5 used for high byte (pins 8-13)
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::disableIrq()
|
||||
{
|
||||
// Disable all interrupts
|
||||
writeRegister8(REG_GPIO_IE_L, 0x00);
|
||||
writeRegister8(REG_GPIO_IE_H, 0x00);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::enableIrq()
|
||||
{
|
||||
// Enable all interrupts
|
||||
writeRegister8(REG_GPIO_IE_L, 0xFF);
|
||||
writeRegister8(REG_GPIO_IE_H, 0x3F); // Pins 8-13
|
||||
}
|
||||
|
||||
uint16_t PY32IOExpander_Class::readDeviceUID()
|
||||
{
|
||||
uint8_t l = readRegister8(REG_UID_L);
|
||||
uint8_t h = readRegister8(REG_UID_H);
|
||||
return (h << 8) | l;
|
||||
}
|
||||
|
||||
uint8_t PY32IOExpander_Class::readVersion()
|
||||
{
|
||||
return readRegister8(REG_VERSION);
|
||||
}
|
||||
|
||||
uint16_t PY32IOExpander_Class::analogRead(uint8_t channel)
|
||||
{
|
||||
if (channel < 1 || channel > 4) return 0;
|
||||
|
||||
// Start conversion
|
||||
// REG_ADC_CTRL: [7:Busy] [6:Start] [2:0:Channel]
|
||||
// Channel mapping: 1->1, 2->2, 3->3, 4->4
|
||||
writeRegister8(REG_ADC_CTRL, (1 << 6) | (channel & 0x07));
|
||||
|
||||
// Wait for busy bit to clear
|
||||
// Simple polling with timeout
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint8_t ctrl = readRegister8(REG_ADC_CTRL);
|
||||
if (!(ctrl & (1 << 7))) {
|
||||
break;
|
||||
}
|
||||
// delay?
|
||||
}
|
||||
|
||||
uint8_t l = readRegister8(REG_ADC_D_L);
|
||||
uint8_t h = readRegister8(REG_ADC_D_H);
|
||||
return (h << 8) | l;
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setPwmDuty(uint8_t channel, uint8_t duty)
|
||||
{
|
||||
if (channel > 3) return;
|
||||
|
||||
// Calculate register address
|
||||
// Channel 0 -> PWM1 (0x1B)
|
||||
// Channel 1 -> PWM2 (0x1D)
|
||||
// Channel 2 -> PWM3 (0x1F)
|
||||
// Channel 3 -> PWM4 (0x21)
|
||||
uint8_t reg_l = REG_PWM1_DUTY_L + (channel * 2);
|
||||
uint8_t reg_h = reg_l + 1;
|
||||
|
||||
// Duty is 8-bit percentage (0-100)? Or 0-255?
|
||||
// m5_io_py32ioexpander uses percentage (0-100) or 12-bit raw.
|
||||
// Let's assume 0-255 for standard Arduino style, but map to 12-bit (0-4095).
|
||||
// 255 -> 4095. val * 4095 / 255 = val * 16 approx.
|
||||
uint16_t duty12 = (uint16_t)duty * 16;
|
||||
if (duty12 > 4095) duty12 = 4095;
|
||||
|
||||
// High byte contains Enable(7) and Polarity(6) bits.
|
||||
// We need to preserve them or set defaults.
|
||||
// Let's enable by default, polarity normal (0).
|
||||
uint8_t h_val = (duty12 >> 8) & 0x0F;
|
||||
h_val |= (1 << 7); // Enable
|
||||
|
||||
writeRegister8(reg_l, duty12 & 0xFF);
|
||||
writeRegister8(reg_h, h_val);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setPwmFrequency(uint16_t freq)
|
||||
{
|
||||
writeRegister8(REG_PWM_FREQ_L, freq & 0xFF);
|
||||
writeRegister8(REG_PWM_FREQ_H, (freq >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setLedCount(uint8_t count)
|
||||
{
|
||||
if (count > 32) count = 32;
|
||||
writeRegister8(REG_LED_CFG, count & 0x3F);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setLedColor(uint8_t index, uint16_t color565)
|
||||
{
|
||||
if (index >= 32) return;
|
||||
uint8_t data[2] = {(uint8_t)(color565 & 0xFF), (uint8_t)((color565 >> 8) & 0xFF)};
|
||||
writeRegister(REG_LED_RAM_START + index * 2, data, 2);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
// RGB888 to RGB565: RRRRRGGG GGGBBBBB
|
||||
uint16_t val = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
|
||||
setLedColor(index, val);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setLedColor(uint8_t index, uint32_t color)
|
||||
{
|
||||
setLedColor(index, (uint8_t)((color >> 16) & 0xFF), (uint8_t)((color >> 8) & 0xFF), (uint8_t)(color & 0xFF));
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::setLedData(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (!data || len == 0) return;
|
||||
if (len > 64) len = 64; // Max 32 LEDs * 2 bytes
|
||||
writeRegister(REG_LED_RAM_START, (uint8_t*)data, len);
|
||||
}
|
||||
|
||||
void PY32IOExpander_Class::refreshLeds()
|
||||
{
|
||||
uint8_t val = readRegister8(REG_LED_CFG);
|
||||
writeRegister8(REG_LED_CFG, val | (1 << 6));
|
||||
}
|
||||
} // namespace m5
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef __M5_PY32IOEXPANDER_CLASS_H__
|
||||
#define __M5_PY32IOEXPANDER_CLASS_H__
|
||||
|
||||
#include <M5Unified.hpp>
|
||||
|
||||
namespace m5 {
|
||||
class PY32IOExpander_Class : public IOExpander_Base {
|
||||
public:
|
||||
static constexpr std::uint8_t DEFAULT_ADDRESS = 0x6F;
|
||||
|
||||
PY32IOExpander_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 100000,
|
||||
m5::I2C_Class* i2c = &m5::In_I2C)
|
||||
: IOExpander_Base(i2c_addr, freq, i2c)
|
||||
{
|
||||
}
|
||||
|
||||
bool begin();
|
||||
|
||||
// IOExpander_Base overrides
|
||||
// false input, true output
|
||||
void setDirection(uint8_t pin, bool direction) override;
|
||||
|
||||
void enablePull(uint8_t pin, bool enablePull) override;
|
||||
|
||||
// false down, true up
|
||||
void setPullMode(uint8_t pin, bool mode) override;
|
||||
|
||||
// false push-pull, true open-drain
|
||||
void setDriveMode(uint8_t pin, bool openDrain);
|
||||
|
||||
void setHighImpedance(uint8_t pin, bool enable) override;
|
||||
|
||||
bool getWriteValue(uint8_t pin) override;
|
||||
|
||||
void digitalWrite(uint8_t pin, bool level) override;
|
||||
|
||||
bool digitalRead(uint8_t pin) override;
|
||||
|
||||
void resetIrq() override;
|
||||
|
||||
void disableIrq() override;
|
||||
|
||||
void enableIrq() override;
|
||||
|
||||
// Extended functionality
|
||||
uint16_t readDeviceUID();
|
||||
uint8_t readVersion();
|
||||
|
||||
// ADC
|
||||
// channel: 1-4
|
||||
uint16_t analogRead(uint8_t channel);
|
||||
|
||||
// PWM
|
||||
// channel: 0-3
|
||||
void setPwmDuty(uint8_t channel, uint8_t duty);
|
||||
void setPwmFrequency(uint16_t freq);
|
||||
|
||||
// LED
|
||||
void setLedCount(uint8_t count);
|
||||
void setLedColor(uint8_t index, uint16_t color565);
|
||||
void setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b);
|
||||
void setLedColor(uint8_t index, uint32_t color);
|
||||
void setLedData(const uint8_t* data, size_t len);
|
||||
void refreshLeds();
|
||||
|
||||
private:
|
||||
void _writeBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin, bool value);
|
||||
bool _readBit(uint8_t reg_l, uint8_t reg_h, uint8_t pin);
|
||||
};
|
||||
} // namespace m5
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/SCS.cpp"
|
||||
"src/SCSCL.cpp"
|
||||
"src/SCSerial.cpp"
|
||||
INCLUDE_DIRS
|
||||
"src"
|
||||
REQUIRES
|
||||
driver
|
||||
esp_timer
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user