Skip to content

Commit

Permalink
Implement simultaneous camera connect (#127)
Browse files Browse the repository at this point in the history
* Add per-camera task and queue.

Lay the foundation for supporting simultaneous, parallel camera
connections.

The UI now sends commands to the control task, which relays them to
per-camera tasks.
This allows the UI and each connected camera to operate asynchronously.

Increase limit maximum clients from 3 to 8.

* Display human readable mobile device names.

Fix #100 by updating to a development cut of NimBLE-Arduino.
Whilst here improve device bond handling, this makes subsequent
pair/forget sequences a little less iffy.

* Implement multi-connect.

M5ez:
Add context to menu item advanced function.

Furble:
Tweak NimBLE settings.
Remove scan duration, now scan forever as we can stop on demand.
Drop global interval_t, load as needed to reduce variable scope.

Add setting to toggle multi-connect.
Modify connection menu to support tagging cameras.
Add ability to connect to all tagged cameras.

Cannot get directed advertising to mobile devices so still disabled.

Multi-connect to mobile devices is super iffy, not recommended for
production use.

* Tweak the code style a little.

Fix minor error in shutter lock formatting, introduced when migrating
from String to std::string.

Also removed unused macros.

* Update clang-format action version.

* Fix m5stack-core build.

* Minor style updates.

Move some more code into std::string native and const a few more
parameters.

* Further C++ refactoring.

Refactor the Camera type into the base Camera class.
Refactor fillSaveName into CameraList class, it is only used there.
Style adjustments.
  • Loading branch information
gkoh authored Sep 18, 2024
1 parent 8afb0e1 commit f146821
Show file tree
Hide file tree
Showing 37 changed files with 737 additions and 410 deletions.
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ ColumnLimit: 100
PointerAlignment: Right
BreakBeforeBinaryOperators: NonAssignment
SpaceBeforeInheritanceColon: false
AlignArrayOfStructures: Left
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
- uses: actions/checkout@v4

- name: Run clang-format style check
uses: jidicula/clang-format-action@v4.11.0
uses: jidicula/clang-format-action@v4.13.0
with:
clang-format-version: '13'
clang-format-version: '17'
check-path: '.'
exclude-regex: 'lib/M5ez/examples'

Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ cameras.

![furble - M5StickC-Plus2](https://github.com/user-attachments/assets/e0eebd87-3ac0-4a8b-871a-014eb8c71395)


The remote uses the camera's native Bluetooth Low Energy interface so additional
adapters are not required.

Expand Down Expand Up @@ -45,6 +44,7 @@ Currently supported features in `furble`:
- focus
- GPS location tagging
- intervalometer
- multi-connect

### Table of Features

Expand Down Expand Up @@ -100,8 +100,10 @@ Connection to mobile devices is a little iffy:
- hit `Scan`
- on the mobile device:
- pair with `furble`
- on `furble` the mobile device bluetooth MAC address should appear as a connectable target
- fixing the target name is tracked in [#100](https://github.com/gkoh/furble/issues/100).
- on `furble` the mobile device should appear as a connectable target if the pairing was successful
- connect to the mobile device to save the pairing
- the devices will remain paired even if you do not connect and save
- forget `furble` on the mobile device to remove such a pair

### GPS Location Tagging

Expand All @@ -124,6 +126,25 @@ Delay and shutter time can be figured with custom or preset values from 0 to 999

When in `Shutter` remote control, holding focus (button B) then release (button A) will engage shutter lock, holding the shutter open until a button is pressed.

### Multi-Connect

Multi-Connect enables simultaneous connection to multiple cameras to synchronise
remote shutter control. Up to 9 (ESP32 hardware limit) cameras can be
simultaneously controlled.

To use:
* Pair with one or more cameras
* Enable `Settings->Multi-Connect`
* In `Connect` select cameras
* Selected cameras will have a `*`
* Select `Connect *`
* Selected cameras will be connected in sequence
* If all cameras are connected, the standard remote control is shown

WARNING:
* mobile device connections are extremely finnicky
* multi-connect involving mobile devices is not well tested and can easily crash

## Motivation

I found current smartphone apps for basic wireless remote shutter control to be
Expand Down
77 changes: 75 additions & 2 deletions include/furble_control.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,89 @@
#include <Camera.h>

#define CONTROL_CMD_QUEUE_LEN (32)
#define TARGET_CMD_QUEUE_LEN (8)

typedef enum {
CONTROL_CMD_SHUTTER_PRESS,
CONTROL_CMD_SHUTTER_RELEASE,
CONTROL_CMD_FOCUS_PRESS,
CONTROL_CMD_FOCUS_RELEASE,
CONTROL_CMD_GPS_UPDATE
CONTROL_CMD_GPS_UPDATE,
CONTROL_CMD_DISCONNECT,
CONTROL_CMD_ERROR
} control_cmd_t;

void control_update_gps(Furble::Camera::gps_t &gps, Furble::Camera::timesync_t &timesync);
namespace Furble {

class Control {
public:
class Target {
public:
Target(Camera *camera);
~Target();

Camera *getCamera(void);
control_cmd_t getCommand(void);
void sendCommand(control_cmd_t cmd);
const Camera::gps_t &getGPS(void);
const Camera::timesync_t &getTimesync(void);

void updateGPS(Camera::gps_t &gps, Camera::timesync_t &timesync);

private:
QueueHandle_t m_Queue = NULL;
Furble::Camera *m_Camera = NULL;
Camera::gps_t m_GPS;
Camera::timesync_t m_Timesync;
};

Control(void);
~Control();

/**
* FreeRTOS control task function.
*/
void task(void);

/**
* Send control command to active connections.
*/
BaseType_t sendCommand(control_cmd_t cmd);

/**
* Update GPS and timesync values.
*/
BaseType_t updateGPS(Camera::gps_t &gps, Camera::timesync_t &timesync);

/**
* Are all active cameras still connected?
*/
bool isConnected(void);

/**
* Get list of connected targets.
*/
const std::vector<std::unique_ptr<Control::Target>> &getTargets(void);

/**
* Disconnect all connected cameras.
*/
void disconnect(void);

/**
* Add specified camera to active target list.
*/
void addActive(Camera *camera);

private:
QueueHandle_t m_Queue = NULL;
std::vector<std::unique_ptr<Control::Target>> m_Targets;
};

}; // namespace Furble

extern "C" {
void control_task(void *param);
}

#endif
5 changes: 3 additions & 2 deletions include/furble_gps.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#ifndef FURBLE_GPS_H
#define FURBLE_GPS_H

#include <Camera.h>
#include <TinyGPS++.h>

#include "furble_control.h"

extern TinyGPSPlus furble_gps;

extern bool furble_gps_enable;

void furble_gps_init(void);
void furble_gps_update(Furble::Camera *camera);
void furble_gps_update(Furble::Control *control);

#endif
4 changes: 2 additions & 2 deletions include/furble_ui.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#ifndef FURBLE_UI_H
#define FURBLE_UI_H

#include <Camera.h>
#include "furble_control.h"

struct FurbleCtx {
Furble::Camera *camera;
Furble::Control *control;
bool reconnected;
};

Expand Down
7 changes: 4 additions & 3 deletions include/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

#include "interval.h"

extern interval_t interval;

void settings_menu_tx_power(void);
esp_power_level_t settings_load_esp_tx_power(void);

Expand All @@ -17,7 +15,10 @@ void settings_menu_gps(void);
void settings_load_interval(interval_t *interval);
void settings_save_interval(interval_t *interval);

void settings_add_interval_items(ezMenu *submenu);
void settings_add_interval_items(ezMenu *submenu, interval_t *interval);
void settings_menu_interval(void);

bool settings_load_multiconnect(void);
void settings_save_multiconnect(bool multiconnect);

#endif
4 changes: 2 additions & 2 deletions include/spinner.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ struct __attribute__((packed)) SpinValue {

void spinner_modify_value(const char *title, bool preset, SpinValue *sv);

std::string sv2str(SpinValue *sv);
unsigned long sv2ms(SpinValue *sv);
std::string sv2str(const SpinValue *sv);
unsigned long sv2ms(const SpinValue *sv);
void ms2hms(unsigned long ms, unsigned int *h, unsigned int *m, unsigned int *s);

#endif
26 changes: 13 additions & 13 deletions lib/M5ez/src/M5ez.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ size_t ezCanvas::write(const uint8_t *buffer, size_t size) {
return size;
}

uint16_t ezCanvas::loop(void *private_data) {
uint16_t ezCanvas::loop(void *context) {
if (_next_scroll && millis() >= _next_scroll) {
ez.setFont(_font);
uint8_t h = ez.fontHeight();
Expand Down Expand Up @@ -907,7 +907,7 @@ void changeCpuPower(bool reduce) {
}
}

uint16_t ezBacklight::loop(void *private_data) {
uint16_t ezBacklight::loop(void *context) {
if (!_backlight_off && _inactivity) {
if (millis() > _last_activity + 30000 * _inactivity) {
_backlight_off = true;
Expand Down Expand Up @@ -955,7 +955,7 @@ void ezBattery::begin() {
}
}

uint16_t ezBattery::loop(void *private_data) {
uint16_t ezBattery::loop(void *context) {
if (!_on)
return 0;
ez.header.draw("battery");
Expand Down Expand Up @@ -1059,11 +1059,12 @@ void M5ez::begin() {
}

void M5ez::yield() {
::yield(); // execute the Arduino yield in the root namespace
vTaskDelay(1); // allow lower priority tasks to run
::yield(); // execute the Arduino yield in the root namespace
M5.update();
for (uint8_t n = 0; n < _events.size(); n++) {
if (millis() > _events[n].when) {
uint16_t r = (_events[n].function)(_events[n].private_data);
uint16_t r = (_events[n].function)(_events[n].context);
if (r) {
_events[n].when = millis() + r - 1;
} else {
Expand All @@ -1074,17 +1075,15 @@ void M5ez::yield() {
}
}

void M5ez::addEvent(uint16_t (*function)(void *private_data),
void *private_data,
uint32_t when /* = 1 */) {
void M5ez::addEvent(uint16_t (*function)(void *context), void *context, uint32_t when /* = 1 */) {
event_t n;
n.function = function;
n.private_data = private_data;
n.context = context;
n.when = millis() + when - 1;
_events.push_back(n);
}

void M5ez::removeEvent(uint16_t (*function)(void *private_data)) {
void M5ez::removeEvent(uint16_t (*function)(void *context)) {
uint8_t n = 0;
while (n < _events.size()) {
if (_events[n].function == function) {
Expand Down Expand Up @@ -1274,13 +1273,14 @@ void ezMenu::txtFont(const GFXfont *font) {
bool ezMenu::addItem(std::string name,
std::string caption,
void (*simpleFunction)() /* = NULL */,
bool (*advancedFunction)(ezMenu *callingMenu) /* = NULL */,
void *context /* = NULL */,
bool (*advancedFunction)(ezMenu *callingMenu, void *context) /* = NULL */,
void (*drawFunction)(ezMenu *callingMenu,
int16_t x,
int16_t y,
int16_t w,
int16_t h) /* = NULL */) {
MenuItem_t new_item = {name, caption, simpleFunction, advancedFunction, drawFunction};
MenuItem_t new_item = {name, caption, context, simpleFunction, advancedFunction, drawFunction};
if (_selected == -1)
_selected = _items.size();
_items.push_back(new_item);
Expand Down Expand Up @@ -1437,7 +1437,7 @@ int16_t ezMenu::_runTextOnce(bool dynamic) {
(_items[_selected].simpleFunction)();
}
if (_items[_selected].advancedFunction) {
if (!(_items[_selected].advancedFunction)(this))
if (!(_items[_selected].advancedFunction)(this, _items[_selected].context))
return 0;
}
return _selected
Expand Down
20 changes: 10 additions & 10 deletions lib/M5ez/src/M5ez.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class ezCanvas: public Print {
uint8_t c); // These three are used to inherit print and println from Print class
virtual size_t write(const char *str);
virtual size_t write(const uint8_t *buffer, size_t size);
static uint16_t loop(void *private_data);
static uint16_t loop(void *context);

private:
static ezTFT tft;
Expand Down Expand Up @@ -297,7 +297,8 @@ class ezMenu {
std::string name,
std::string caption = "",
void (*simpleFunction)() = NULL,
bool (*advancedFunction)(ezMenu *callingMenu) = NULL,
void *context = nullptr,
bool (*advancedFunction)(ezMenu *callingMenu, void *context) = NULL,
void (*drawFunction)(ezMenu *callingMenu, int16_t x, int16_t y, int16_t w, int16_t h) = NULL);
bool deleteItem(int16_t index);
bool deleteItem(std::string name);
Expand Down Expand Up @@ -338,8 +339,9 @@ class ezMenu {
struct MenuItem_t {
std::string name;
std::string caption;
void *context;
void (*simpleFunction)();
bool (*advancedFunction)(ezMenu *callingMenu);
bool (*advancedFunction)(ezMenu *callingMenu, void *context);
void (*drawFunction)(ezMenu *callingMenu, int16_t x, int16_t y, int16_t w, int16_t h);
};
std::vector<MenuItem_t> _items;
Expand Down Expand Up @@ -425,7 +427,7 @@ class ezBacklight {
static void menu();
static void inactivity(uint8_t half_minutes);
static void activity();
static uint16_t loop(void *private_data);
static uint16_t loop(void *context);

private:
static uint8_t _brightness;
Expand All @@ -450,7 +452,7 @@ class ezBacklight {
class ezBattery {
public:
static void begin();
static uint16_t loop(void *private_data);
static uint16_t loop(void *context);
static uint8_t getTransformedBatteryLevel();
static uint16_t getBatteryBarColor(uint8_t batteryLevel);

Expand All @@ -471,7 +473,7 @@ class ezBattery {

struct event_t {
uint16_t (*function)(void *);
void *private_data;
void *context;
uint32_t when;
};

Expand Down Expand Up @@ -503,10 +505,8 @@ class M5ez {

static void yield();

static void addEvent(uint16_t (*function)(void *),
void *private_data = nullptr,
uint32_t when = 1);
static void removeEvent(uint16_t (*function)(void *private_data));
static void addEvent(uint16_t (*function)(void *), void *context = nullptr, uint32_t when = 1);
static void removeEvent(uint16_t (*function)(void *context));
static void redraw();

// ez.msgBox
Expand Down
Loading

0 comments on commit f146821

Please sign in to comment.