Skip to content
Snippets Groups Projects
Commit 861d604a authored by schneider's avatar schneider
Browse files

Merge branch 'schneider/default-main-master' into 'master'

Default main app selector

See merge request card10/firmware!375
parents 6e823291 9b737f6a
No related branches found
No related tags found
No related merge requests found
......@@ -40,4 +40,6 @@ Option name Type Description
``execute_elf`` Boolean Allow running of binary :ref:`l0dables`. These files can be nefarious, so this option is off by default.
--------------- ---------- -----------
``timezone`` String Timezone for card10; must be of format ``[+-]HHMM``. Examples: ``+0800``, ``-0200``
--------------- ---------- -----------
``default_app`` String Full path to the exectutable file of the default application. If this option is not set,``apps/analog_clock/__init__.py`` is used.
=============== ========== ===========
......@@ -62,6 +62,8 @@ class ColorExample(rst.Directive):
color = self.arguments[0]
html_text = '<div style="width: 30px;height: 30px;background: {};border: black 1px solid;border-radius: 15px;"></div>'
return [nodes.raw("", html_text.format(color), format="html")]
# }}}
# -- Options for HTML output ------------------------------------------------- {{{
......@@ -112,6 +114,7 @@ autodoc_mock_imports = [
"sys_display",
"sys_leds",
"sys_max30001",
"sys_config",
"ucollections",
"urandom",
"utime",
......
......@@ -27,6 +27,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
pycardium/max30001
pycardium/buttons
pycardium/color
pycardium/config
pycardium/display
pycardium/gpio
pycardium/leds
......
``config`` - Configuration
==========================
The ``config`` module provides functions to interact with card10's
configuration file (``card10.cfg``).
.. automodule:: config
:members:
......@@ -145,6 +145,7 @@ typedef _Bool bool;
#define API_CONFIG_GET_STRING 0x130
#define API_CONFIG_GET_INTEGER 0x131
#define API_CONFIG_GET_BOOLEAN 0x132
#define API_CONFIG_SET_STRING 0x133
/* clang-format on */
typedef uint32_t api_int_id_t;
......@@ -2060,5 +2061,21 @@ API(API_CONFIG_GET_BOOLEAN, int epic_config_get_boolean(const char *key, bool *v
API(API_CONFIG_GET_STRING, int epic_config_get_string(const char *key, char *buf, size_t buf_len));
/**
* Write a string to the configuration file.
*
* :param char* key: Name of the option to write
* :param char* value: The value to write
* :return: `0` on success or a negative value if an error occured. Possivle
* errors:
*
* - ``-EINVAL``: Parameters out of range
* - ``-ENOENT``: Key already exists but can not be read
* - ``-EIO`` : Unspecified I/O error
* - Any fopen/fread/fwrite/fclose related error code
*
* .. versionadded:: 1.16
*/
API(API_CONFIG_SET_STRING, int epic_config_set_string(const char *key, const char *value));
#endif /* _EPICARDIUM_H */
......@@ -7,9 +7,11 @@
#include <stdbool.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
#include <stdio.h>
#define MAX_LINE_LENGTH 80
#define KEYS_PER_BLOCK 16
......@@ -125,25 +127,39 @@ static void add_config_pair(
slot->value_offset = value_offset;
}
static void trim(char *str)
{
char *start = str;
while (*start && !isgraph(*start))
start++;
if (strlen(start) > 0) {
char *end = start + strlen(start) - 1;
while (*end && !isgraph(*end))
end--;
end[1] = 0;
}
memmove(str, start, strlen(start) + 1);
}
// parses one line of the config file
static void
parse_line(char *line, char *eol, int line_number, size_t line_offset)
static void parse_line(char *line, int line_number, size_t line_offset)
{
char *line_start = line;
char *line_start = line;
char *value_start = strchr(line, '=') + 1;
//skip leading whitespace
while (*line && isspace((int)*line))
++line;
trim(line);
char *key = line;
if (*key == '#') {
//printf(line);
if (*line == '#') {
//skip comments
return;
}
char *eq = strchr(line, '=');
if (!eq) {
if (*key) {
if (*line) {
LOG_WARN(
"card10.cfg",
"line %d: syntax error",
......@@ -152,26 +168,19 @@ parse_line(char *line, char *eol, int line_number, size_t line_offset)
}
return;
}
*eq = 0;
char *key = line;
trim(key);
char *e_key = eq - 1;
//skip trailing whitespace in key
while (e_key > key && isspace((int)*e_key))
--e_key;
e_key[1] = '\0';
if (*key == '\0') {
LOG_WARN("card10.cfg", "line %d: empty key", line_number);
return;
}
char *value = eq + 1;
//skip leading whitespace
while (*value && isspace((int)*value))
++value;
char *e_val = eol - 1;
//skip trailing whitespace
while (e_val > value && isspace((int)*e_val))
--e_val;
trim(value);
if (*value == '\0') {
LOG_WARN(
"card10.cfg",
......@@ -182,21 +191,44 @@ parse_line(char *line, char *eol, int line_number, size_t line_offset)
return;
}
size_t value_offset = value - line_start + line_offset;
size_t value_offset = value_start - line_start + line_offset;
add_config_pair(key, value, line_number, value_offset);
}
// convert windows line endings to unix line endings.
// we don't care about the extra empty lines
static void convert_crlf_to_lflf(char *buf, int n)
typedef struct {
int line_number;
int file_offset;
int line_start;
char line[MAX_LINE_LENGTH + 1];
int line_length;
} parser_state;
int parse_character(char c, parser_state *s)
{
while (n--) {
if (*buf == '\r') {
*buf = '\n';
if (c != '\r' && c != '\n') {
if (s->line_length == MAX_LINE_LENGTH) {
LOG_WARN(
"card10.cfg",
"line:%d: too long - aborting",
s->line_number
);
return -1;
}
s->line[s->line_length++] = c;
} else {
s->line[s->line_length] = 0;
//printf("New line: %s (%d %d)\n", s->line, s->line_number, s->line_start);
parse_line(s->line, s->line_number, s->line_start);
s->line_length = 0;
s->line_start = s->file_offset + 1;
if (c == '\n') {
s->line_number++;
}
buf++;
}
s->file_offset++;
return 0;
}
// parses the entire config file
......@@ -213,77 +245,21 @@ void load_config(void)
);
return;
}
char buf[MAX_LINE_LENGTH + 1];
int line_number = 0;
size_t file_offset = 0;
char buf[128];
int nread;
parser_state s;
memset(&s, 0, sizeof(s));
s.line_number = 1;
do {
nread = epic_file_read(fd, buf, MAX_LINE_LENGTH);
convert_crlf_to_lflf(buf, nread);
if (nread < MAX_LINE_LENGTH) {
//add fake EOL to ensure termination
buf[nread++] = '\n';
nread = epic_file_read(fd, buf, sizeof(buf));
int i;
for (i = 0; i < nread; i++) {
parse_character(buf[i], &s);
}
//zero-terminate buffer
buf[nread] = '\0';
char *line = buf;
char *eol = NULL;
int last_eol = 0;
while (line) {
//line points one character past the last (if any) '\n' hence '- 1'
last_eol = line - buf - 1;
eol = strchr(line, '\n');
++line_number;
if (eol) {
*eol = '\0';
parse_line(line, eol, line_number, file_offset);
file_offset += eol - line + 1;
line = eol + 1;
continue;
}
if (line == buf) {
//line did not fit into buf
LOG_WARN(
"card10.cfg",
"line:%d: too long - aborting",
line_number
);
return;
}
int seek_back = last_eol - nread;
} while (nread == sizeof(buf));
parse_character('\n', &s);
LOG_DEBUG(
"card10.cfg",
"nread, last_eol, seek_back: %d,%d,%d",
nread,
last_eol,
seek_back
);
assert(seek_back <= 0);
if (!seek_back) {
break;
}
int rc = epic_file_seek(fd, seek_back, SEEK_CUR);
if (rc < 0) {
LOG_ERR("card10.cfg", "seek failed, aborting");
return;
}
char newline;
rc = epic_file_read(fd, &newline, 1);
if (rc < 0 || (newline != '\n' && newline != '\r')) {
LOG_ERR("card10.cfg", "read failed, aborting");
LOG_DEBUG(
"card10.cfg",
"read failed at read-back of newline: rc: %d read: %d",
rc,
(int)newline
);
return;
}
break;
}
} while (nread == MAX_LINE_LENGTH);
epic_file_close(fd);
}
......@@ -355,10 +331,11 @@ int epic_config_get_string(const char *key, char *buf, size_t buf_len)
return -ENOENT;
}
char *eol = strchr(buf, '\n');
if (eol) {
*eol = '\0';
}
char *end = buf;
while (*end && !iscntrl(*end))
end++;
*end = 0;
trim(buf);
return 0;
}
......@@ -389,10 +366,10 @@ int epic_config_get_boolean(const char *key, bool *value)
char buf[MAX_LINE_LENGTH];
epic_config_get_string(key, buf, MAX_LINE_LENGTH);
if (!strcmp(buf, "true")) {
if (!strcasecmp(buf, "true")) {
*value = true;
return 0;
} else if (!strcmp(buf, "false")) {
} else if (!strcasecmp(buf, "false")) {
*value = false;
return 0;
}
......@@ -411,3 +388,256 @@ bool config_get_boolean_with_default(const char *key, bool default_value)
return value;
}
}
int epic_config_set_string(const char *key, const char *value_in)
{
char value[MAX_LINE_LENGTH + 1];
if (strlen(key) > MAX_LINE_LENGTH) {
return -EINVAL;
}
/* TODO: Change interface of trim to take the buffer and size directly */
if (strlen(value_in) > MAX_LINE_LENGTH) {
return -EINVAL;
}
strcpy(value, value_in);
trim(value);
if (snprintf(NULL, 0, "\n%s = %s\n", key, value) > MAX_LINE_LENGTH) {
return -EINVAL;
}
/* Check if key is sane. No control characters, spaces, equal signs or pounds allowed */
for (size_t i = 0; i < strlen(key); i++) {
char c = key[i];
if (!isgraph(c) || c == '=' || c == '#') {
return -EINVAL;
}
}
/* Check if value is sane. No control characters allowed */
for (size_t i = 0; i < strlen(value); i++) {
char c = value[i];
if (!isprint(c)) {
return -EINVAL;
}
}
config_slot *slot = find_config_slot(key);
bool present = slot && slot->value_offset;
int ret = 0;
if (!present) {
/* Easy case: We simply add the new option at the
* end of the file. */
char buf[MAX_LINE_LENGTH];
/* Leading new line because I'm lazy */
ret = snprintf(buf, sizeof(buf), "\n%s = %s\n", key, value);
if (ret < 0 || ret >= (int)sizeof(buf)) {
return -EINVAL;
}
int fd = epic_file_open("card10.cfg", "a");
if (fd < 0) {
LOG_DEBUG(
"card10.cfg",
"open for appending failed: %s (%d)",
strerror(-fd),
fd
);
return fd;
}
int write_ret = epic_file_write(fd, buf, strlen(buf));
if (write_ret < 0) {
LOG_DEBUG(
"card10.cfg",
"writing failed: %s (%d)",
strerror(-write_ret),
write_ret
);
}
if (write_ret < (int)strlen(buf)) {
LOG_DEBUG(
"card10.cfg",
"writing failed to write all bytes (%d of %d)",
write_ret,
strlen(buf)
);
}
ret = epic_file_close(fd);
if (ret < 0) {
LOG_DEBUG(
"card10.cfg",
"close failed: %s (%d)",
strerror(-ret),
ret
);
}
if (write_ret < 0) {
ret = write_ret;
}
if (ret < 0) {
goto out;
}
if (write_ret < (int)strlen(buf)) {
LOG_DEBUG(
"card10.cfg",
"writing failed to write all bytes (%d of %d)",
write_ret,
strlen(buf)
);
ret = -EIO;
goto out;
}
} else {
/* Complex case: The value is already somewhere in the file.
* We do not want to lose existing formatting or comments.
* Solution: Copy parts of the file, insert new value, copy
* rest, rename.
*/
char buf[MAX_LINE_LENGTH + 1];
int fd1 = -1;
int fd2 = -1;
ret = epic_config_get_string(key, buf, sizeof(buf));
size_t nread = read_config_offset(
slot->value_offset, buf, sizeof(buf)
);
if (nread == 0) {
LOG_DEBUG("card10.cfg", "could not read old value", );
goto complex_out;
}
char *end = buf;
while (*end && (!iscntrl(*end) || isblank(*end)))
end++;
*end = 0;
int old_len = strlen(buf);
fd1 = epic_file_open("card10.cfg", "r");
if (fd1 < 0) {
LOG_DEBUG(
"card10.cfg",
"open for read failed: %s (%d)",
strerror(-fd1),
fd1
);
ret = fd1;
goto complex_out;
}
fd2 = epic_file_open("card10.nfg", "w");
if (fd2 < 0) {
LOG_DEBUG(
"card10.nfg",
"open for writing failed: %s (%d)",
strerror(-fd2),
fd2
);
ret = fd2;
goto complex_out;
}
/* Copy over slot->value_offset bytes */
int i = slot->value_offset;
while (i > 0) {
int n = i > (int)sizeof(buf) ? (int)sizeof(buf) : i;
ret = epic_file_read(fd1, buf, n);
if (ret < 0) {
LOG_DEBUG(
"card10.cfg",
"read failed: rc: %d",
ret
);
goto complex_out;
}
int ret2 = epic_file_write(fd2, buf, ret);
if (ret2 < 0) {
ret = ret2;
LOG_DEBUG(
"card10.nfg",
"write failed: rc: %d",
ret
);
goto complex_out;
}
i -= ret;
}
/* Insert new value into the new file */
ret = epic_file_write(fd2, value, strlen(value));
if (ret < 0) {
LOG_DEBUG("card10.nfg", "write failed: rc: %d", ret);
goto complex_out;
}
/* Skip the old value inside the old file */
epic_file_seek(fd1, old_len, SEEK_CUR);
/* Copy the rest of the old file to the new file */
while (true) {
int ret = epic_file_read(fd1, buf, sizeof(buf));
if (ret == 0) {
break;
}
if (ret < 0) {
LOG_DEBUG(
"card10.cfg",
"read failed: rc: %d",
ret
);
goto complex_out;
}
int ret2 = epic_file_write(fd2, buf, ret);
if (ret2 < 0) {
ret = ret2;
LOG_DEBUG(
"card10.nfg",
"write failed: rc: %d",
ret
);
goto complex_out;
}
if (ret < (int)sizeof(buf)) {
break;
}
}
complex_out:
if (fd1 >= 0) {
epic_file_close(fd1);
}
if (fd2 >= 0) {
int ret2 = epic_file_close(fd2);
if (ret >= 0) {
ret = ret2;
}
}
if (ret >= 0) {
epic_file_unlink("card10.cfg");
epic_file_rename("card10.nfg", "card10.cfg");
}
}
out:
/* Reload config so the new key or the changed value is available */
load_config();
return ret < 0 ? ret : 0;
}
......@@ -12,6 +12,7 @@
#include "epicardium.h"
#include "modules/filesystem.h"
#include "modules/config.h"
#include "usb/cdcacm.h"
#include "usb/mass_storage.h"
......@@ -139,6 +140,7 @@ int epic_usb_shutdown(void)
esb_deinit();
if (s_fsDetached) {
fatfs_attach();
load_config();
}
return 0;
}
......@@ -155,6 +157,7 @@ int epic_usb_cdcacm(void)
esb_deinit();
if (s_fsDetached) {
fatfs_attach();
load_config();
}
return esb_init(&s_cfg_cdcacm);
}
......
......@@ -3,7 +3,13 @@ import os
def main():
# Try loading analog clock
default_app = "apps/analog_clock/__init__.py"
try:
import config
default_app = config.get_string("default_app")
except OSError:
default_app = "apps/analog_clock/__init__.py"
try:
with open(default_app, "r"):
pass
......
......@@ -90,6 +90,24 @@ def usb_mode(disp):
os.exit(0)
class SubMenu(simple_menu.Menu):
timeout = 30.0
def on_select(self, name, index):
print(name)
if name == "Make Default":
self.disp.clear(color.CHAOSBLUE)
self.disp.print("Making", posx=0, posy=20)
self.disp.print(self.app.name, posx=0, posy=40)
self.disp.print("default", posx=0, posy=60)
self.disp.update()
import config
config.set_string("default_app", self.app.path)
utime.sleep(1)
self.exit()
class MainMenu(simple_menu.Menu):
timeout = 30.0
......@@ -112,6 +130,14 @@ class MainMenu(simple_menu.Menu):
utime.sleep(1.0)
os.exit(1)
def on_long_select(self, app, index):
print("Long press\n")
if app.path not in ["main.py", "USB_STORAGE_FLAG"]:
# sm = SubMenu(("Make Default", "Fav", "Unfav", "Delete", "Exit"))
sm = SubMenu(("Make Default", "Exit", "", ""))
sm.app = app
sm.run()
def on_timeout(self):
try:
f = open("main.py")
......
......@@ -3,6 +3,7 @@ name = 'pycardium'
modsrc = files(
'modules/bhi160-sys.c',
'modules/buttons.c',
'modules/sys_config.c',
'modules/fat_file.c',
'modules/fat_reader_import.c',
'modules/gpio.c',
......
import sys_config
def set_string(key, value):
"""
Write a string to the configuration file ``card10.cfg``.
Both ``key`` and ``value`` must be strings or must be
convertible to a string using the ``str()`` function.
``key`` must not contain spaces, control characters (including tabs),
number signs ans equal signs.
``value` must not contain control characters (including tabs).
Neither is allowed to contain the sub-string ``execute_elf``.
The key/value pair is immediately written to the configuration
file (``card10.cfg``). After the file is written, configuration
is read again and the new value is available via ``config.get_string``.
:param str key: Name of the configuration option.
:param str value: Value to write.
:raises OSError: If writing to the configuration file failed.
:raises OSError: If key or value contain illegal characters.
:raises ValueError: If key or value contain the sub-string ``execute_elf``.
.. versionadded:: 1.16
"""
sys_config.set_string(str(key), str(value))
def get_string(key):
"""
Read a string from the configuration file ``card10.cfg``.
``key`` must be a string or must be convertible to a string using
the ``str()`` function.
:param str key: Name of the configuration option.
:rtype: str
:returns: Value of the configuration option.
:raises OSError: if the key is not present in the configuration.
.. versionadded:: 1.16
"""
return sys_config.get_string(str(key))
python_modules = files(
'config.py',
'bhi160.py',
'bme680.py',
'color.py',
......
......@@ -141,6 +141,18 @@ class Menu:
"""
pass
def on_long_select(self, item, index):
"""
Hook when an item as selected using a long press.
The given ``index`` was selected with a long SELECT button press. Overwrite
this function in your menu to perform an action on select.
:param item: The item which was selected.
:param int index: Index into the ``entries`` list of the ``item``.
"""
pass
def on_select(self, item, index):
"""
Hook when an item as selected.
......@@ -238,9 +250,7 @@ class Menu:
offset + 20,
col=self.color_1 if index % 2 == 0 else self.color_2,
)
self.disp.print(
string, posx=14, posy=offset, fg=self.color_text, bg=None,
)
self.disp.print(string, posx=14, posy=offset, fg=self.color_text, bg=None)
def draw_menu(self, offset=0):
"""
......@@ -318,8 +328,18 @@ class Menu:
print("Exception during menu.on_scroll():")
sys.print_exception(e)
elif ev == self.button_select:
t0 = utime.time()
long_press = False
while buttons.read(buttons.TOP_RIGHT) > 0:
if utime.time() - t0 > 1:
long_press = True
break
try:
self.on_select(self.entries[self.idx], self.idx)
if long_press:
self.on_long_select(self.entries[self.idx], self.idx)
else:
self.on_select(self.entries[self.idx], self.idx)
self.select_time = utime.time_ms()
except _ExitMenuException:
raise
......
......@@ -188,3 +188,7 @@ Q(MAX86150)
/* ws2812 */
Q(ws2812)
Q(set_all)
Q(config)
Q(set_string)
Q(get_string)
#include "epicardium.h"
#include "py/obj.h"
#include "py/runtime.h"
#include <string.h>
#include <strings.h>
#include <stdbool.h>
static void extract_string(
mp_obj_t obj, char *buf, size_t buf_len, const char *error_message
) {
size_t len;
const char *str_ptr = mp_obj_str_get_data(obj, &len);
/*
* The string retrieved from MicroPython is not NULL-terminated so we
* first need to copy it and add a NULL-byte.
*/
if (len > (buf_len - 1)) {
mp_raise_ValueError(error_message);
}
memcpy(buf, str_ptr, len);
buf[len] = '\0';
}
static mp_obj_t mp_config_set_string(mp_obj_t key_in, mp_obj_t value_in)
{
const char *const forbidden_key = "execute_elf";
char key_str[128], value_str[128];
extract_string(key_in, key_str, sizeof(key_str), "key too long");
extract_string(
value_in, value_str, sizeof(value_str), "value too long"
);
if (strstr(key_str, forbidden_key) != NULL ||
strstr(value_str, forbidden_key) != NULL) {
/* A Permission Error might be nice but is not available in MP */
mp_raise_ValueError("Not allowed");
}
int status = epic_config_set_string(key_str, value_str);
if (status < 0) {
mp_raise_OSError(-status);
}
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_2(set_string_obj, mp_config_set_string);
static mp_obj_t mp_config_get_string(mp_obj_t key_in)
{
char key_str[128], value_str[128];
extract_string(key_in, key_str, sizeof(key_str), "key too long");
int status =
epic_config_get_string(key_str, value_str, sizeof(value_str));
if (status < 0) {
mp_raise_OSError(-status);
}
mp_obj_t ret = mp_obj_new_str(value_str, strlen(value_str));
return ret;
}
static MP_DEFINE_CONST_FUN_OBJ_1(get_string_obj, mp_config_get_string);
static const mp_rom_map_elem_t config_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_config) },
{ MP_ROM_QSTR(MP_QSTR_set_string), MP_ROM_PTR(&set_string_obj) },
{ MP_ROM_QSTR(MP_QSTR_get_string), MP_ROM_PTR(&get_string_obj) },
};
static MP_DEFINE_CONST_DICT(config_module_globals, config_module_globals_table);
// Define module object.
const mp_obj_module_t config_module = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&config_module_globals,
};
/* This is a special macro that will make MicroPython aware of this module */
/* clang-format off */
MP_REGISTER_MODULE(MP_QSTR_sys_config, config_module, MODULE_CONFIG_ENABLED);
......@@ -64,6 +64,7 @@ int mp_hal_trng_read_int(void);
#define MODULE_UTIME_ENABLED (1)
#define MODULE_VIBRA_ENABLED (1)
#define MODULE_WS2812_ENABLED (1)
#define MODULE_CONFIG_ENABLED (1)
/*
* This port is intended to be 32-bit, but unfortunately, int32_t for
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment