diff --git a/README.md b/README.md index 017ee7e..19b5a0d 100644 --- a/README.md +++ b/README.md @@ -3,42 +3,44 @@ # Stabilizer Firmware +## Hardware + +[![Hardware](https://github.com/sinara-hw/Stabilizer/wiki/Stabilizer_v1.0_top_small.jpg)](https://github.com/sinara-hw/Stabilizer) + +## Applications + +The Stabilizer firmware offeres a library of hardware and software functionality +exposing input/output, timing, and digital signal processing features. +An application can compose and configure these hardware and software components +to implement different use cases. Several applications are provides by default + +### Dual-IIR + ![Flow diagram](stabilizer_pid.svg) -![Hardware](https://github.com/sinara-hw/Stabilizer/wiki/Stabilizer_v1.0_top_small.jpg) - -## Features - * dual channel * SPI ADC * SPI DAC -* 500 kHz rate, timed sampling -* 2 µs latency, unmatched between channels +* up to 800 kHz rate, timed sampling +* down to 2 µs latency * f32 IIR math * generic biquad (second order) IIR filter * anti-windup * derivative kick avoidance -## Limitations/TODOs +### Lockin external -* Fixed AFE gains -* The IP and MAC address are [hardcoded](src/hardware/configuration.rs) -* Expose configurable limits -* 100Base-T only -* Digital IO, GPIO header, AFE header, EEM header are not handled - -## Hardware - -See https://github.com/sinara-hw/Stabilizer +### Lockin internal ## Minimal bootstrapping documentation * Clone or download this * Get [rustup](https://rustup.rs/) -* Get [cargo-binutils](https://github.com/rust-embedded/cargo-binutils/) * `rustup target add thumbv7em-none-eabihf` * `cargo build --release` -* Do not try the debug (default) mode. It is guaranteed to panic. +* When using debug (non `--release`) mode, increase the sample interval significantly. + The added error checking code and missing optimizations may lead to the code + missing deadlines and panicing. ### Using Cargo-embed @@ -55,30 +57,24 @@ See https://github.com/sinara-hw/Stabilizer * `openocd -f stabilizer.cfg` and leave it running * `cargo run --release` -[^swd]: Build a cable: connect a standard 8 conductor ribbon with the wires numbered - `1-8` to the pins on the St-Link v2 single row 2.54mm connector as `647513(82)` - (`(i)` marks an unused wire) - and to the [1.27mm dual row](https://www.digikey.de/short/p41h0n) on Stabilizer as `657483x2x1` - (`x` marks an unused pin, enumeration is standard for dual row, as in the - schematic). - It's just folding the ribbon between wires `5` and `6`. The signals on the ribbon - are then `NRST,TDI,TDO,TCK,TMS,3V3,GND,GND`. - ### Using USB-DFU * Install the DFU USB tool (`dfu-util`) * Connect to the Micro USB connector below the RJ45 * Short JC2/BOOT +* Get [cargo-binutils](https://github.com/rust-embedded/cargo-binutils/) * `cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin` or `arm-none-eabi-objcopy -O binary target/thumbv7em-none-eabihf/release/dual-iir dual-iir.bin` * `dfu-util -a 0 -s 0x08000000:leave -D dual-iir.bin` ### Using ST-Link virtual mass storage +* Get [cargo-binutils](https://github.com/rust-embedded/cargo-binutils/) * `cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin` or `arm-none-eabi-objcopy -O binary target/thumbv7em-none-eabihf/release/dual-iir dual-iir.bin` * Connect the ST-Link debugger * copy `dual-iir.bin` to the `NODE_H743ZI` USB disk ## Protocol -Stabilizer can be configured via MQTT under the topic `stabilizer/settings/`. Refer to +Stabilizer can be configured via MQTT. Refer to [`miniconf`](https://github.com/quartiq/miniconf) for more information about topics. +A basic command line interface is available in [`miniconf.py`](miniconf.py). diff --git a/miniconf.py b/miniconf.py new file mode 100644 index 0000000..019ebcb --- /dev/null +++ b/miniconf.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +""" +Author: Vertigo Designs, Ryan Summers + Robert Jördens + +Description: Provides an API for controlling Miniconf devices over MQTT. +""" +import argparse +import asyncio +import json +import logging + +from gmqtt import Client as MqttClient + +logger = logging.getLogger(__name__) + + +class Miniconf: + """An asynchronous API for controlling Miniconf devices using MQTT.""" + + @classmethod + async def create(cls, prefix, broker): + """Create a connection to the broker and a Miniconf device using it.""" + client = MqttClient(client_id='') + await client.connect(broker) + return cls(client, prefix) + + def __init__(self, client, prefix): + """Constructor. + + Args: + client: A connected MQTT5 client. + prefix: The MQTT toptic prefix of the device to control. + """ + self.client = client + self.prefix = prefix + self.inflight = {} + self.client.on_message = self._handle_response + self.client.subscribe(f'{prefix}/response/#') + + def _handle_response(self, _client, topic, payload, *_args, **_kwargs): + """Callback function for when messages are received over MQTT. + + Args: + _client: The MQTT client. + topic: The topic that the message was received on. + payload: The payload of the message. + """ + if topic not in self.inflight: + # TODO use correlation_data to distinguish clients and requests + logger.warning('Unexpected response on topic: %s', topic) + return + + self.inflight[topic].set_result(payload.decode('ascii')) + del self.inflight[topic] + + async def command(self, path, value): + """Write the provided data to the specified path. + + Args: + path: The path to write the message to. + value: The value to write to the path. + + Returns: + The received response to the command. + """ + setting_topic = f'{self.prefix}/settings/{path}' + response_topic = f'{self.prefix}/response/{path}' + if response_topic in self.inflight: + # TODO use correlation_data to distinguish clients and requests + raise NotImplementedError( + 'Only one in-flight message per topic is supported') + + value = json.dumps(value) + logger.info('Sending %s to "%s"', value, setting_topic) + fut = asyncio.get_running_loop().create_future() + self.inflight[response_topic] = fut + self.client.publish(setting_topic, payload=value, qos=0, retain=True, + response_topic=response_topic) + return await fut + + +def main(): + parser = argparse.ArgumentParser( + description='Miniconf command line interface.', + epilog='''Example: + %(prog)s -v -b mqtt dt/sinara/stabilizer afe/0 '"G10"' + ''') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase logging verbosity') + parser.add_argument('--broker', '-b', default='mqtt', type=str, + help='The MQTT broker address') + parser.add_argument('prefix', type=str, + help='The MQTT topic prefix of the target') + parser.add_argument('path', type=str, + help='The setting path to configure') + parser.add_argument('value', type=str, + help='The value of setting in JSON format') + + args = parser.parse_args() + + logging.basicConfig( + format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', + level=logging.WARN - 10*args.verbose) + + loop = asyncio.get_event_loop() + + async def configure_settings(): + interface = await Miniconf.create(args.prefix, args.broker) + response = await interface.command(args.path, json.loads(args.value)) + print(f"Response: {response}") + + loop.run_until_complete(configure_settings()) + + +if __name__ == '__main__': + main() diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 66c2a11..5d51632 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -6,10 +6,7 @@ use stm32h7xx_hal as hal; use stabilizer::hardware; -use miniconf::{ - embedded_nal::{IpAddr, Ipv4Addr}, - minimq, Miniconf, MqttInterface, -}; +use miniconf::{minimq, Miniconf, MqttInterface}; use serde::Deserialize; use dsp::iir; @@ -62,13 +59,20 @@ const APP: () = { let mqtt_interface = { let mqtt_client = { - let broker = IpAddr::V4(Ipv4Addr::new(10, 34, 16, 1)); - minimq::MqttClient::new(broker, "", stabilizer.net.stack) - .unwrap() + minimq::MqttClient::new( + hardware::design_parameters::MQTT_BROKER.into(), + "", + stabilizer.net.stack, + ) + .unwrap() }; - MqttInterface::new(mqtt_client, "stabilizer", Settings::default()) - .unwrap() + MqttInterface::new( + mqtt_client, + "dt/sinara/stabilizer", + Settings::default(), + ) + .unwrap() }; // Enable ADC/DAC events diff --git a/src/bin/lockin-external.rs b/src/bin/lockin-external.rs index 5b85489..306ae45 100644 --- a/src/bin/lockin-external.rs +++ b/src/bin/lockin-external.rs @@ -4,10 +4,7 @@ use generic_array::typenum::U4; -use miniconf::{ - embedded_nal::{IpAddr, Ipv4Addr}, - minimq, Miniconf, MqttInterface, -}; +use miniconf::{minimq, Miniconf, MqttInterface}; use serde::Deserialize; use dsp::{Accu, Complex, ComplexExt, Lockin, RPLL}; @@ -77,14 +74,19 @@ const APP: () = { let (mut stabilizer, _pounder) = setup(c.core, c.device); let mqtt_interface = { - let mqtt_client = { - let broker = IpAddr::V4(Ipv4Addr::new(10, 34, 16, 10)); - minimq::MqttClient::new(broker, "", stabilizer.net.stack) - .unwrap() - }; + let mqtt_client = minimq::MqttClient::new( + design_parameters::MQTT_BROKER.into(), + "", + stabilizer.net.stack, + ) + .unwrap(); - MqttInterface::new(mqtt_client, "lockin", Settings::default()) - .unwrap() + MqttInterface::new( + mqtt_client, + "dt/sinara/lockin", + Settings::default(), + ) + .unwrap() }; let settings = Settings::default(); diff --git a/src/hardware/design_parameters.rs b/src/hardware/design_parameters.rs index ddc0614..9a4279b 100644 --- a/src/hardware/design_parameters.rs +++ b/src/hardware/design_parameters.rs @@ -49,3 +49,6 @@ pub const ADC_SAMPLE_TICKS: u16 = 1 << ADC_SAMPLE_TICKS_LOG2; // The desired ADC sample processing buffer size. pub const SAMPLE_BUFFER_SIZE_LOG2: u8 = 3; pub const SAMPLE_BUFFER_SIZE: usize = 1 << SAMPLE_BUFFER_SIZE_LOG2; + +// The MQTT broker IPv4 address +pub const MQTT_BROKER: [u8; 4] = [10, 34, 16, 10]; diff --git a/stabilizer.py b/stabilizer.py deleted file mode 100644 index f302067..0000000 --- a/stabilizer.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/python -""" -Author: Vertigo Designs, Ryan Summers - -Description: Provides an API for controlling Stabilizer over Miniconf (MQTT). -""" -import argparse -import asyncio -import json -import logging - -from gmqtt import Client as MqttClient - - -class MiniconfApi: - """ An asynchronous API for controlling Miniconf devices using the MQTT control interface. """ - - @classmethod - async def create(cls, identifier, broker): - """ Create a connection to MQTT for communication with the device. """ - client = MqttClient(client_id='') - await client.connect(broker) - return cls(client, identifier) - - - def __init__(self, client, identifier): - """ Consructor. - - Args: - client: A connected MQTT5 client. - identifier: The ID of the device to control. - """ - self.client = client - self.identifier = identifier - self.client.on_message = self._handle_response - self.inflight_settings = dict() - self.logger = logging.getLogger('stabilizer.miniconf') - - self.client.subscribe(f'{identifier}/feedback/#') - - - def _handle_response(self, _client, topic, payload, *_args, **_kwargs): - """ Callback function for when messages are received over MQTT. - - Args: - _client: The MQTT client. - topic: The topic that the message was received on. - payload: The payload of the message. - """ - if topic not in self.inflight_settings: - self.logger.warning('Unknown response topic: %s', topic) - return - - # Indicate a response was received for the provided topic. - self.inflight_settings[topic].set_result(payload.decode('ascii')) - - - async def command(self, path, value): - """ Write the provided data to the specified path. - - Args: - setting: The path to write the message to. - value: The value to write to the path. - - Returns: - The received response to the command. - """ - setting_topic = f'{self.identifier}/{path}' - response_topic = f'{self.identifier}/feedback/{path}' - assert response_topic not in self.inflight_settings, \ - 'Only one in-flight message per topic is supported' - - self.logger.debug('Sending %s to "%s"', value, setting_topic) - self.inflight_settings[response_topic] = asyncio.get_running_loop().create_future() - self.client.publish(setting_topic, payload=value, qos=0, retain=False, - response_topic=response_topic) - - response = await self.inflight_settings[response_topic] - del self.inflight_settings[response_topic] - - return response - - -async def configure_settings(args): - """ Configure an RF channel. """ - logger = logging.getLogger('stabilizer') - - # Establish a communication interface with stabilizer. - interface = await MiniconfApi.create(args.stabilizer, args.broker) - - request = None - - # In the exceptional case that this is a terminal value, there is no key available and only a - # single value. - if len(args.values) == 1 and '=' not in args.values[0]: - if args.values[0][0].isalpha(): - request = args.values[0] - else: - request = json.loads(args.values[0]) - else: - # Convert all of the values into a key-value list. - request = dict() - for pair in args.values: - key, value = pair.split('=') - request[str(key)] = json.loads(value) - logger.debug('Parsed request: %s', request) - - response = await interface.command(f'settings/{args.setting}', json.dumps(request)) - logger.info(response) - - -def main(): - """ Main program entry point. """ - parser = argparse.ArgumentParser(description='Stabilizer settings modification utility') - parser.add_argument('--stabilizer', type=str, default='stabilizer', - help='The identifier of the stabilizer to configure') - parser.add_argument('--setting', required=True, type=str, help='The setting path to configure') - parser.add_argument('--broker', default='10.34.16.1', type=str, help='The MQTT broker address') - parser.add_argument('values', nargs='+', type=str, - help='The value of settings. key=value list or a single value is accepted.') - parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging') - - args = parser.parse_args() - - logger = logging.getLogger('stabilizer') - logger.setLevel(logging.INFO) - - if args.verbose: - logger.setLevel(logging.DEBUG) - - logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s') - - loop = asyncio.get_event_loop() - loop.run_until_complete(configure_settings(args)) - - -if __name__ == '__main__': - main()