Skip to content

Commit

Permalink
Add general EEPROM usage and application file guidance in README
Browse files Browse the repository at this point in the history
  • Loading branch information
eberseth committed Oct 30, 2023
1 parent 00d4700 commit 818bb22
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 0 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,59 @@ This application must be built with device OS version 4.0.2 and above.
5. Connect your device
6. Compile & Flash!

### Expansion Card EEPROM Usage

As this project comes out-of-the-box, there is an expectation that an EEPROM is present and programmed on the expansion card. This EEPROM is used to help differentiate between various Particle supplied expansion cards. As there may be very different functionality between card SKUs, the SKU contained in the EEPROM is beneficial to steer Monitor Edge functionality between one card versus another.

The EEPROM has a simple structure programmed into the first couple of 32-byte pages. Details and helper functions can be found in [lib/edge/src/eeprom_helper.h](lib/edge/src/eeprom_helper.h). The basic structure is comprised of the following fields:

```cpp
struct ExpansionEeprom1 {
uint16_t size; ///< Size of entire ExpansionEeprom
///< structure. LSB, MSB
uint8_t revision; ///< Revision number of this hardware
///< starting from 1
char sku[29]; ///< SKU name with null termination
uint8_t serial[16]; ///< 128-bit serial number MSB->LSB
uint8_t reserved[16]; ///< Page boundary filler
} __attribute__((packed));
```
* The `size` field is simply filled with `sizeof(ExpansionEeprom1)`.
* `revision` usually follows the hardware revision and set during manufacturing stages.
* The `sku` string field is important for expansion card differentiation and is evaluated during the boot/setup stage of execution.
* `serial` may or not be programmed at manufacturing and is used to identify specific expansion cards. This field is ignored if the EEPROM has built-in unique serial numbers.
Everything after the 64-byte `ExpansionEeprom1` structure is open for user purposes including calibration tables or manufacturing data.
Common `sku` strings include:
* "EXP1_IO_BASIC_485CAN" is populated for IO expansion cards.
* "EXP1_PROTO" is populated for Particle's protoboard expansion cards.
If no EEPROM SKU is found then Monitor Edge will boot with generic Tracker Edge-like functionality.
Look at source where the EEPROM is read and SKU handled in [src/user_setup.cpp](src/user_setup.cpp) within the `user_init()` function for more detailed implementation details.
### General Directory and File Layout
`src/` and `lib/` directories are typically used for Particle application projects. Library project files from other repositories and source that is general purpose for most applications are found in the `lib/` directory. Custom appliction source code that vary from user-to-user, application-to-application are typically located in the `src/` directory. All source code can be modified for any application use but it is good practice to modify in `src/` and leave `lib/` alone so that it easier to sync future bug fixes as well as features in this project.
In addition to `main.cpp`, there are four files that are left for user customization in the `src/` directory. They include:
```
src/
main.cpp <- Created with very minimal code. It is the entry point to setup() and loop().
user_config.h <- Used to consolidate control over what is compiled in the project.
user_setup.cpp <- Contains common Monitor One elements.
user_io.cpp <- Very specific to the IO expansion card.
user_modbus.cpp <- Also very specific to the IO expansion card.
```
As mentioned, `main.cpp` contains very minimal code and was historically meant to be a clean slate for new users to develop their applications. It was coded very minimally so that the user is aware that more complex stuff is happening undernealth but their application source is more navigatable and updates with the Monitor Edge github repository don't result in a bunch of git conflicts.
Monitor One/Edge allows for expansion through the internal headers that Tracker One/Edge didn't really accomodate. As a result, various expansion cards can be installed which brings up the dilemma of having one reference application serve mutliple off-the-shelf Particle expansion SKUs. The `user_setup.cpp` file attempts to solve this by initializing common Monitor One peripherals such as the RGB status LED and push button switch. The user is free to change the functionality of the push button, change RGB status states, as well as control the side mounted RGB LEDs. Two functions, `user_init()` and `user_loop()` are overriding weak, bare declarations inside of [lib/edge/src/edge.cpp](lib/edge/src/edge.cpp). If the user wishes to delete `user_setup.cpp`, `user_io.cpp`, and `user_modbus.cpp` there will be no ill consequences and Monitor Edge will revert to basic Tracker Edge functionality.
Both `user_io.cpp` and `user_modbus.cpp` are there to support the IO expansion board. They add Particle cloud functions, variables, and configuration parameters that are relevant only to the IO expansion card. They can be deleted entirely if not used or simply kept from compilation setting defines in `user_config.h`.
### CONTRIBUTE
Want to contribute to the Particle tracker edge firmware project? Follow [this link](CONTRIBUTING.md) to find out how.
Expand Down
116 changes: 116 additions & 0 deletions scripts/schema-to-cpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#
# Copyright (c) 2023 Particle Industries, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import json
import sys

def load_json_schema(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)

def to_camel_case(snake_str):
components = snake_str.split('_')
return components[0] + ''.join(x.title() for x in components[1:])

def generate_cpp_code(json_schema, search_name):
constants = []
enums = []
structs = []

def generate_enum(enum_type, enum_values):
enums.append(f"enum class {enum_type} {{")
for enum_value in enum_values:
enums.append(f" e_{enum_value},")
enums.append("};\n")

def generate_struct(struct_name, properties):
struct_code = [f"struct {struct_name}_t {{"]
for key, value in properties.items():
if 'type' not in value:
continue
full_key = to_camel_case(key)
if value['type'] == 'object':
struct_code.append(f" {full_key} {full_key.lower()};")
generate_struct(full_key, value['properties'])
elif value['type'] == 'string' and 'enum' in value:
enum_type = f"{struct_name}_{full_key}Type"
generate_enum(enum_type, value['enum'])
struct_code.append(f" {enum_type} {full_key.lower()};")
elif value['type'] == 'integer':
struct_code.append(f" int32_t {full_key.lower()};")
elif value['type'] == 'number':
struct_code.append(f" double {full_key.lower()};")
elif value['type'] == 'boolean':
struct_code.append(f" bool {full_key.lower()};")
struct_code.append("};\n")
structs.append('\n'.join(struct_code))

def recurse(properties, parent_key=''):
found = False
for key, value in properties.items():
if 'type' not in value:
continue
full_key = f"{parent_key}_{to_camel_case(key)}" if parent_key else to_camel_case(key)
if value['type'] == 'object':
if key == search_name:
found = True
generate_struct(to_camel_case(key), value['properties'])
break
elif recurse(value['properties'], full_key):
found = True
break
elif value['type'] == 'string' and 'enum' in value:
enum_type = to_camel_case(key)
generate_enum(enum_type, value['enum'])
if 'default' in value:
constants.append(f"constexpr {enum_type} {full_key.upper()}_DEFAULT = {enum_type}::e_{value['default'].upper()};\n")
elif value['type'] == 'integer':
if 'default' in value:
constants.append(f"constexpr int32_t {full_key.upper()}_DEFAULT = {value['default']};")
if 'minimum' in value:
constants.append(f"constexpr int32_t {full_key.upper()}_MIN = {value['minimum']};")
if 'maximum' in value:
constants.append(f"constexpr int32_t {full_key.upper()}_MAX = {value['maximum']};")
elif value['type'] == 'number':
if 'default' in value:
constants.append(f"constexpr double {full_key.upper()}_DEFAULT = {value['default']};")
if 'minimum' in value:
constants.append(f"constexpr double {full_key.upper()}_MIN = {value['minimum']};")
if 'maximum' in value:
constants.append(f"constexpr double {full_key.upper()}_MAX = {value['maximum']};")
elif value['type'] == 'boolean':
if 'default' in value:
constants.append(f"constexpr bool {full_key.upper()}_DEFAULT = {'true' if value['default'] else 'false'};")
return found

recurse(json_schema['properties'], search_name)
return '\n'.join(enums + constants + structs)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert JSON schema to C++ code")
parser.add_argument("file_path", help="Path to the JSON schema file")
parser.add_argument("search_name", help="Root name for the C++ structs")
args = parser.parse_args()

try:
json_schema = load_json_schema(args.file_path)
cpp_code = generate_cpp_code(json_schema, args.search_name)
print(cpp_code)
except FileNotFoundError:
print("Error: The specified file does not exist.", file=sys.stderr)
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}", file=sys.stderr)
69 changes: 69 additions & 0 deletions scripts/schema-to-csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#
# Copyright (c) 2023 Particle Industries, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import json
import sys

def load_json_schema(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)

def parse_schema(json_schema, delim=',', parent_key='', parent_version=''):
rows = []
for key, value in json_schema.get('properties', {}).items():
full_key = f"{parent_key}/{key}" if parent_key else key
version = value.get('minimumFirmwareVersion', parent_version)
if value.get('type') == 'object':
rows.extend(parse_schema(value, delim, full_key, version))
else:
type_ = value.get('type', '')
if delim == ',':
description = '"' + value.get('description', '').replace('\n', ' ').replace('\t', ' ') + '"'
else:
description = value.get('description', '').replace('\n', ' ').replace('\t', ' ')
rows.append(f"{full_key}{delim}{type_}{delim}{version}{delim}{description}")
return rows

def save_to_file(rows, file=None):
for row in rows:
file.write(row + "\n")

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert JSON schema to CSV")
parser.add_argument("input", help="Input file name")
parser.add_argument("--output", help="Output file name (prints to stdout if not provided)")
parser.add_argument("-t", "--tab", help="Use tab delimiter (TSV) instead of comma (CSV)", action='store_true')
args = parser.parse_args()

delim = '\t' if args.tab else ','

try:
json_schema = load_json_schema(args.input)
rows = ["Name" + delim + "Type" + delim + "Version" + delim + "Description"]
rows.extend(parse_schema(json_schema, delim))

if args.output:
with open(args.output, 'w', encoding='utf-8') as file:
save_to_file(rows, file)
print("Conversion completed successfully! Output written to:", args.output, file=sys.stdout)
else:
save_to_file(rows, sys.stdout)

except FileNotFoundError:
print("Error: The specified file does not exist.", file=sys.stderr)
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}", file=sys.stderr)

0 comments on commit 818bb22

Please sign in to comment.