Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating documentation, refactoring reflected power interlock threshold #113

Merged
merged 5 commits into from
Jan 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,55 @@ jobs:
- beta
steps:
- uses: actions/checkout@v2

- name: Install Rust ${{ matrix.toolchain }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
target: thumbv7em-none-eabihf
override: true
- name: cargo check
components: llvm-tools-preview

- name: Install Binutils
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-binutils

- name: Style Check
uses: actions-rs/cargo@v1
with:
command: check
args: --verbose
- name: cargo build

- name: Build [Debug]
uses: actions-rs/cargo@v1
with:
command: build
- name: cargo build release

- name: Build [Release]
uses: actions-rs/cargo@v1
with:
command: build
args: --release

- name: Generate Release
uses: actions-rs/cargo@v1
with:
command: objcopy
args: --release --verbose -- -O binary booster-release.bin

- name: Upload Release
uses: actions/upload-artifact@v2
if: ${{ matrix.toolchain == 'stable' }} &&
(${{ github.ref == 'refs/heads/master' }} ||
${{ github.ref == 'refs/heads/develop' }})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to still upload artifacts even for PRs into develop, so this may need a revisit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK for now. We can restrict it once it becomes too much.

with:
name: Firmware Images
path: |
target/*/release/booster
booster-release.bin

compile-unstable:
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ heapless = { version = "0.5.5", features = ["serde"] }
bit_field = "0.10.0"
debounced-pin = "0.3.0"
serde = {version = "1.0", features = ["derive"], default-features = false }
serde-json-core = "0.1"
serde-json-core = "0.2"
logos = { version = "0.11.4", default-features = false, features = ["export_derive"] }
bbqueue = "0.4.10"
usbd-serial = "0.1.0"
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ Additionally, the USB port allows the user to:
* Reset booster
* Enter DFU mode for upgrading firmware

## Ethernet
## Booster Units

Booster uses SI units (Volt, Ampere, Celsius) for telemetry messages and properties. Power measurements are specified in dBm.

## Ethernet Telemetry and Control

Booster uses MQTT for telemetry and control of RF channels. All booster MQTT topics are prefixed
with the booster ID, which defaults to a combination of `booster` and the MAC address of the device.
Expand All @@ -127,6 +131,18 @@ property is provided, Booster will respond by default to `<ID>/log`.

For a reference implementation of the API to control booster over MQTT, refer to `booster.py`

**Note**: When using mosquitto, the channel telemetry can be read using:
```
mosquitto_sub -h mqtt -v -t <ID>/ch<N>
```
Note in the above that <N> is an integer between 0 and 7 (inclusive). <ID> is as specified above.

### Booster Properties

Throughout the code, any reference to a "Property" refers to a value that is configurable by the
user. Telemetry outputs, such as temperature or power measurements, are not considered to be
properties of Booster.

### Special Note on Booster properties

When reading or writing Booster properties of `<ID>/channel/read` or `<ID>/channel/write`, the
Expand Down Expand Up @@ -214,7 +230,11 @@ the channel when Booster boots. Note that saving channel settings overwrites any
**Prerequisites**
* Ensure `dfu-util` is installed. On Ubuntu, install it from `apt` using `sudo apt-get install
dfu-util`
* If building your own firmware, install `cargo-binutils`: `cargo install cargo-binutils`
* If building your own firmware, [`cargo-binutils`](https://github.com/rust-embedded/cargo-binutils#installation)) must be installed:
```
cargo install cargo-binutils
rustup component add llvm-tools-preview
```

The following instructions describe the process of uploading a new firmware image over the DFU
Bootloader USB interface.
Expand All @@ -224,7 +244,7 @@ Bootloader USB interface.
- Note: You may also use the latest [pre-built](https://github.com/quartiq/booster/releases) assets instead of building firmware.

1. Generate the binary file for your firmware build: `cargo objcopy -- -O binary booster.bin`
- Note: If you built with `--release`, replace `debug` with `release` in the above command.
- Note: If you built with `--release`, use the commmand: `cargo objcopy --release -- -O binary booster.bin`

1. Reset Booster into DFU mode:
- Insert a pin into the DFU Bootloader hole to press the DFU button
Expand Down
27 changes: 11 additions & 16 deletions booster.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

class PropertyId(enum.Enum):
""" Represents a property ID for Booster RF channels. """
InterlockThresholds = 'InterlockThresholds'
OutputInterlockThreshold = 'OutputInterlockThreshold'
OutputPowerTransform = 'OutputPowerTransform'
InputPowerTransform = 'InputPowerTransform'
ReflectedPowerTransform = 'ReflectedPowerTransform'
Expand Down Expand Up @@ -166,7 +166,7 @@ async def read_property(self, channel, prop):
request = generate_request(prop=prop.value, channel=CHANNEL[channel])

response = await self.command("channel/read", request)
return json.loads(response['data'].replace("'", '"'))
return json.loads(response['data'])


async def write_property(self, channel, prop_id, value):
Expand All @@ -178,7 +178,7 @@ async def write_property(self, channel, prop_id, value):
value: The value to write to the property.
"""
request = generate_request(prop=prop_id.value, channel=CHANNEL[channel],
data=json.dumps(value).replace('"', "'"))
data=json.dumps(value))

await self.command("channel/write", request)

Expand Down Expand Up @@ -273,20 +273,17 @@ async def channel_configuration(args):
await interface.enable_channel(args.channel)
print(f'Channel {args.channel} enabled')

if args.thresholds:
thresholds = {
'output': args.thresholds[0],
'reflected': args.thresholds[1],
}
await interface.write_property(args.channel, PropertyId.InterlockThresholds, thresholds)
print(f'Channel {args.channel}: Output power threshold = {args.thresholds[0]} dBm, '
f'Reflected power interlock threshold = {args.thresholds[1]} dBm')
if args.threshold is not None:
await interface.write_property(args.channel,
PropertyId.OutputInterlockThreshold,
args.threshold)
print(f'Channel {args.channel}: Output power threshold = {args.threshold} dBm')

if args.bias:
if args.bias is not None:
vgs, ids = await interface.set_bias(args.channel, args.bias)
print(f'Channel {args.channel}: Vgs = {vgs:.3f} V, Ids = {ids * 1000:.2f} mA')

if args.tune:
if args.tune is not None:
vgs, ids = await interface.tune_bias(args.channel, args.tune)
print(f'Channel {args.channel}: Vgs = {vgs:.3f} V, Ids = {ids * 1000:.2f} mA')

Expand Down Expand Up @@ -317,9 +314,7 @@ def main():
help='Tune the RF channel bias current to the provided value')
parser.add_argument('--enable', action='store_true', help='Enable the RF channel')
parser.add_argument('--disable', action='store_true', help='Disable the RF channel')
parser.add_argument('--thresholds', type=float, nargs=2,
help='The interlock thresholds in the following order: '
'<output> <reflected>')
parser.add_argument('--threshold', type=float, help='The output interlock threshold')
parser.add_argument('--save', action='store_true', help='Save the RF channel configuration')

loop = asyncio.get_event_loop()
Expand Down
73 changes: 31 additions & 42 deletions src/mqtt_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ use embedded_hal::blocking::delay::DelayUs;
use heapless::{consts, String};
use minimq::{Property, QoS};

use crate::rf_channel::{
InterlockThresholds, Property as ChannelProperty, PropertyId as ChannelPropertyId,
};
use crate::rf_channel::{Property as ChannelProperty, PropertyId as ChannelPropertyId};

use crate::linear_transformation::LinearTransformation;

Expand All @@ -37,8 +35,8 @@ impl PropertyReadResponse {
pub fn okay(prop: ChannelProperty) -> String<consts::U256> {
// Serialize the property.
let data: String<consts::U64> = match prop {
ChannelProperty::InterlockThresholds(thresholds) => {
serde_json_core::to_string(&thresholds).unwrap()
ChannelProperty::OutputInterlockThreshold(threshold) => {
serde_json_core::to_string(&threshold).unwrap()
}
ChannelProperty::InputPowerTransform(transform) => {
serde_json_core::to_string(&transform).unwrap()
Expand All @@ -51,21 +49,11 @@ impl PropertyReadResponse {
}
};

let mut response = Self {
let response = Self {
code: 200,
data: String::new(),
data: String::from(data.as_str()),
};

// Convert double quotes to single in the encoded property. This gets around string escape
// sequences.
for byte in data.as_str().chars() {
if byte == '"' {
response.data.push('\'').unwrap();
} else {
response.data.push(byte).unwrap();
}
}

serde_json_core::to_string(&response).unwrap()
}
}
Expand All @@ -83,33 +71,34 @@ impl PropertyWriteRequest {
/// # Returns
/// The property if it could be deserialized. Otherwise, an error is returned.
pub fn property(&self) -> Result<ChannelProperty, Error> {
// Convert single quotes to double in the property data.
let mut data: String<consts::U64> = String::new();
for byte in self.data.as_str().chars() {
if byte == '\'' {
data.push('"').unwrap();
} else {
data.push(byte).unwrap();
}
}

// Convert the property
let prop = match self.prop {
ChannelPropertyId::InterlockThresholds => ChannelProperty::InterlockThresholds(
serde_json_core::from_str::<InterlockThresholds>(&data)
.map_err(|_| Error::Invalid)?,
),
ChannelPropertyId::OutputInterlockThreshold => {
// Due to a bug in serde-json-core, trailing data must be present for a float to be
// properly parsed. For more information, refer to:
// https://github.com/rust-embedded-community/serde-json-core/issues/47
let mut data: String<consts::U65> = String::from(self.data.as_str());
data.push(' ').unwrap();
ChannelProperty::OutputInterlockThreshold(
serde_json_core::from_str::<f32>(&data)
.map_err(|_| Error::Invalid)?
.0,
)
}
ChannelPropertyId::OutputPowerTransform => ChannelProperty::OutputPowerTransform(
serde_json_core::from_str::<LinearTransformation>(&data)
.map_err(|_| Error::Invalid)?,
serde_json_core::from_str::<LinearTransformation>(&self.data)
.map_err(|_| Error::Invalid)?
.0,
),
ChannelPropertyId::InputPowerTransform => ChannelProperty::InputPowerTransform(
serde_json_core::from_str::<LinearTransformation>(&data)
.map_err(|_| Error::Invalid)?,
serde_json_core::from_str::<LinearTransformation>(&self.data)
.map_err(|_| Error::Invalid)?
.0,
),
ChannelPropertyId::ReflectedPowerTransform => ChannelProperty::ReflectedPowerTransform(
serde_json_core::from_str::<LinearTransformation>(&data)
.map_err(|_| Error::Invalid)?,
serde_json_core::from_str::<LinearTransformation>(&self.data)
.map_err(|_| Error::Invalid)?
.0,
),
};

Expand Down Expand Up @@ -331,7 +320,7 @@ impl ControlState {
/// A String response indicating the result of the request.
fn handle_channel_update(message: &[u8], channels: &mut BoosterChannels) -> String<consts::U256> {
let request = match serde_json_core::from_slice::<ChannelRequest>(message) {
Ok(data) => data,
Ok((data, _)) => data,
Err(_) => return Response::error_msg("Failed to decode data"),
};
channels
Expand Down Expand Up @@ -369,7 +358,7 @@ fn handle_channel_property_read(
channels: &mut BoosterChannels,
) -> String<consts::U256> {
let request = match serde_json_core::from_slice::<PropertyReadRequest>(message) {
Ok(data) => data,
Ok((data, _)) => data,
Err(_) => return Response::error_msg("Failed to decode read request"),
};

Expand All @@ -392,8 +381,8 @@ fn handle_channel_property_write(
channels: &mut BoosterChannels,
) -> String<consts::U256> {
let request = match serde_json_core::from_slice::<PropertyWriteRequest>(message) {
Ok(data) => data,
Err(_) => return Response::error_msg("Failed to decode read request"),
Ok((data, _)) => data,
Err(_) => return Response::error_msg("Failed to decode write request"),
};

let property = match request.property() {
Expand Down Expand Up @@ -422,7 +411,7 @@ fn handle_channel_bias(
delay: &mut impl DelayUs<u16>,
) -> String<consts::U256> {
let request = match serde_json_core::from_slice::<ChannelBiasRequest>(message) {
Ok(data) => data,
Ok((data, _)) => data,
Err(_) => return Response::error_msg("Failed to decode data"),
};

Expand Down
4 changes: 4 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use super::hal;

use embedded_hal::{blocking::delay::DelayUs, digital::v2::OutputPin};

// Booster hardware channels are capable of withstanding up to 1W of reflected RF power. This
// corresponds with a value of 30 dBm.
pub const MAXIMUM_REFLECTED_POWER_DBM: f32 = 30.0;

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
cortex_m::interrupt::disable();
Expand Down
Loading