Compare commits
6 Commits
0e3328aac1
...
5b62dd300b
Author | SHA1 | Date | |
---|---|---|---|
5b62dd300b | |||
32eeab2c66 | |||
0aef4f295d | |||
0034d2e9e7 | |||
aebb3a58f0 | |||
8e6e483f92 |
33
README.md
33
README.md
@ -5,6 +5,39 @@ improved passive sonar data acquisition system. The goal of this system is
|
|||||||
simply to be able to capture pinger data, filtered by the preprocessor board,
|
simply to be able to capture pinger data, filtered by the preprocessor board,
|
||||||
via ethernet, at significantly faster speed than the previous system.
|
via ethernet, at significantly faster speed than the previous system.
|
||||||
|
|
||||||
|
## Pysonar
|
||||||
|
|
||||||
|
This provides a library to interface with the FPGA, both pulling data and configuring it. To install, simply `cd`
|
||||||
|
into this repo and:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install ./
|
||||||
|
```
|
||||||
|
|
||||||
|
A CLI/console to test configuration is provided as `sonar_config`. Usage can be found with
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sonar_config --help
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note: This is a pure python package. In an ideal world, I would have implemented all protocol
|
||||||
|
implementation in rust, and exported via PyO3, to keep potential changes in sync, but it's not
|
||||||
|
worth the implementation effort and potential installation difficulty here.*
|
||||||
|
|
||||||
|
## Hacky hacks to patch into migen
|
||||||
|
|
||||||
|
migen.fhdl.simplify.py (line 81): add the attributeerror exception
|
||||||
|
|
||||||
|
```
|
||||||
|
for port in mem.ports:
|
||||||
|
try:
|
||||||
|
sync = f.sync[port.clock.cd]
|
||||||
|
except KeyError:
|
||||||
|
sync = f.sync[port.clock.cd] = []
|
||||||
|
except AttributeError:
|
||||||
|
sync = f.sync["sys"]
|
||||||
|
```
|
||||||
|
|
||||||
## Repo Layout
|
## Repo Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
7
firmware/Cargo.lock
generated
7
firmware/Cargo.lock
generated
@ -50,6 +50,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc-any"
|
||||||
|
version = "2.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "774646b687f63643eb0f4bf13dc263cb581c8c9e57973b6ddf78bda3994d88df"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "critical-section"
|
name = "critical-section"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@ -102,6 +108,7 @@ dependencies = [
|
|||||||
name = "fw"
|
name = "fw"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"crc-any",
|
||||||
"defmt",
|
"defmt",
|
||||||
"embedded-hal",
|
"embedded-hal",
|
||||||
"panic-halt",
|
"panic-halt",
|
||||||
|
@ -10,12 +10,13 @@ riscv-rt = "0.11.0"
|
|||||||
riscv = "0.10.1"
|
riscv = "0.10.1"
|
||||||
panic-halt = "0.2.0"
|
panic-halt = "0.2.0"
|
||||||
embedded-hal = "0.2.7"
|
embedded-hal = "0.2.7"
|
||||||
defmt = {version = "0.3.4", features = ["encoding-raw"] }
|
defmt = { version = "0.3.4", features = ["encoding-raw"] }
|
||||||
|
crc-any = { version = "2.4", default-features = false }
|
||||||
|
|
||||||
[dependencies.smoltcp]
|
[dependencies.smoltcp]
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["medium-ethernet", "proto-ipv4", "socket-icmp", "defmt"]
|
features = ["medium-ethernet", "proto-ipv4", "socket-tcp", "socket-icmp", "defmt"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
|
97
firmware/src/command_interface.rs
Normal file
97
firmware/src/command_interface.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use smoltcp::socket::tcp::{Socket, State};
|
||||||
|
|
||||||
|
use crate::proto::{
|
||||||
|
serialize_response_error, serialize_response_value, PacketParser, ResponsePacket, Settings, ErrorCodes
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandInterface {
|
||||||
|
parser: PacketParser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandInterface {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
parser: PacketParser::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the command interface
|
||||||
|
pub fn run(&mut self, sock: &mut Socket) {
|
||||||
|
// Note that we don't attempt to check status of the connection. All of
|
||||||
|
// the smoltcp functions handle the error cases gracefully. Recv
|
||||||
|
// provides a buffer of length 0 when there is nothing to receive, so we
|
||||||
|
// are good there. We just reset on any smoltcp errors.
|
||||||
|
|
||||||
|
// TODO should put packet parsing behind a fn with the smoltcp result, then on smoltcp error
|
||||||
|
// we reconfigure here instead of calling it 4 times
|
||||||
|
|
||||||
|
// Try and parse a packet
|
||||||
|
let res = sock.recv(|rx_buf| {
|
||||||
|
let (res, processed_bytes) = self.parser.try_parse(rx_buf);
|
||||||
|
|
||||||
|
(processed_bytes, res.ok())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for socket errors
|
||||||
|
let packet = match res {
|
||||||
|
Ok(Some(packet)) => packet,
|
||||||
|
// No packet to process
|
||||||
|
Ok(None) => return,
|
||||||
|
// Any socket error means we just want to drop the connection and re-connect
|
||||||
|
Err(_) => {
|
||||||
|
Self::reestablish_socket(sock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check that setting is valid
|
||||||
|
let setting = match Settings::try_from(packet.setting) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(()) => {
|
||||||
|
// Write out an error packet
|
||||||
|
let result = sock.send(|tx_buf| {
|
||||||
|
if tx_buf.len() < 8 {
|
||||||
|
return (0, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = serialize_response_error(packet.setting, ErrorCodes::InvalidSetting);
|
||||||
|
&tx_buf[0..8].copy_from_slice(&response);
|
||||||
|
return (8, ());
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(_) = result {
|
||||||
|
// TX error, close socket and reconnect
|
||||||
|
Self::reestablish_socket(sock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO validate setting values
|
||||||
|
|
||||||
|
// TODO handle actually changing/getting settings in all the ways
|
||||||
|
// For temp testing, return values sent
|
||||||
|
match sock.send(|tx_buf| {
|
||||||
|
if tx_buf.len() < 8 {
|
||||||
|
// Since this is a low-BW configuration socket, we assume we have space, and drop
|
||||||
|
// the response otherwise. If this is an issue, may re-think this and queue commands but
|
||||||
|
// for now this is fine.
|
||||||
|
return (0, ());
|
||||||
|
}
|
||||||
|
let response = serialize_response_value(setting, packet.value);
|
||||||
|
&tx_buf[0..8].copy_from_slice(&response);
|
||||||
|
return (8, ());
|
||||||
|
}) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(_) => {
|
||||||
|
Self::reestablish_socket(sock);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reestablish_socket(sock: &mut Socket) {
|
||||||
|
sock.abort();
|
||||||
|
sock.listen(2000);
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,8 @@ mod mcp4726;
|
|||||||
mod uart;
|
mod uart;
|
||||||
mod litex_uart;
|
mod litex_uart;
|
||||||
mod logging;
|
mod logging;
|
||||||
|
mod proto;
|
||||||
|
mod command_interface;
|
||||||
|
|
||||||
const MAC: [u8; 6] = [0xA0, 0xBB, 0xCC, 0xDD, 0xEE, 0xF0];
|
const MAC: [u8; 6] = [0xA0, 0xBB, 0xCC, 0xDD, 0xEE, 0xF0];
|
||||||
|
|
||||||
|
229
firmware/src/proto.rs
Normal file
229
firmware/src/proto.rs
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
//! # Configuration protocol
|
||||||
|
//!
|
||||||
|
//! Configuration and runtime management is done over a separate TCP port, to
|
||||||
|
//! simplify management. It follows a simple binary protocol, described here.
|
||||||
|
//!
|
||||||
|
//! The protocol is packet-based, 2 bytes start sequence, 1 byte command, 4 byte
|
||||||
|
//! data, 1 byte CRC. CRC includes the start sequence, and is MAXIM-DOW. Chosen
|
||||||
|
//! because the crc python package had it as well.
|
||||||
|
//!
|
||||||
|
//! The 4 bytes of data are little-endian.
|
||||||
|
//!
|
||||||
|
//! For the settings that can be set, see [Settings].
|
||||||
|
//!
|
||||||
|
//! ## Sending a command
|
||||||
|
//!
|
||||||
|
//! The command is simply the index of the setting to read or write, with the
|
||||||
|
//! top bit set to indicate a write. Set the value to 0 during a read.
|
||||||
|
//!
|
||||||
|
//! ## Receiving a response
|
||||||
|
//!
|
||||||
|
//! The command is the index of the setting given, with the top bit set if it's
|
||||||
|
//! an error. The value is either the value saved in the setting, or an error
|
||||||
|
//! code. Error codes are defined in [ErrorCodes]
|
||||||
|
|
||||||
|
/// Start sequence to indicate the beginning of a packet.
|
||||||
|
const START_SEQUENCE: [u8; 2] = [0xDE, 0xAD];
|
||||||
|
|
||||||
|
/// Possible return values from a command
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum ErrorCodes {
|
||||||
|
/// Command accepted
|
||||||
|
Success = 0,
|
||||||
|
/// Value was not valid for the provided setting
|
||||||
|
ValueOutOfRange = 1,
|
||||||
|
/// Setting index provided does not exist
|
||||||
|
InvalidSetting = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setting indices
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Settings {
|
||||||
|
/// Trigger threshold (in ADC counts, peak to peak is 1024)
|
||||||
|
TriggerThreshold = 0,
|
||||||
|
/// Number of samples that diff must be higher than threshold to trigger
|
||||||
|
TriggerPeriod = 1,
|
||||||
|
/// Value to decay difference every DecayPeriod cycles
|
||||||
|
DecayValue = 2,
|
||||||
|
/// The number of cycles between every decay
|
||||||
|
DecayPeriod = 3,
|
||||||
|
/// Gain setting of preprocessor (value in dB)
|
||||||
|
Gain = 4,
|
||||||
|
/// Center frequency to set preprocessor filter to (value in Hz)
|
||||||
|
CenterFreq = 5,
|
||||||
|
/// Sampling enabled, 1 to enable, 0 to disable
|
||||||
|
SamplingEnabled = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Start sequence incorrect, no packet
|
||||||
|
BadStartSequence,
|
||||||
|
/// CRC was bad
|
||||||
|
BadCrc,
|
||||||
|
/// State machine error
|
||||||
|
StateMachineError,
|
||||||
|
/// No packet found in data
|
||||||
|
NoPacket,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for Settings {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(Self::TriggerThreshold),
|
||||||
|
1 => Ok(Self::TriggerPeriod),
|
||||||
|
2 => Ok(Self::DecayValue),
|
||||||
|
3 => Ok(Self::DecayPeriod),
|
||||||
|
4 => Ok(Self::Gain),
|
||||||
|
5 => Ok(Self::CenterFreq),
|
||||||
|
6 => Ok(Self::SamplingEnabled),
|
||||||
|
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CommandPacket {
|
||||||
|
pub is_write: bool,
|
||||||
|
pub setting: u8,
|
||||||
|
pub value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResponsePacket {
|
||||||
|
pub is_error: bool,
|
||||||
|
pub setting: u8,
|
||||||
|
pub value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a valid response
|
||||||
|
pub fn serialize_response_value(setting: Settings, value: u32) -> [u8; 8] {
|
||||||
|
serialize_response(setting as u8, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize an error response
|
||||||
|
pub fn serialize_response_error(setting: u8, error: ErrorCodes) -> [u8; 8] {
|
||||||
|
serialize_response(0x80 | setting, error as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_response(command: u8, value: u32) -> [u8; 8] {
|
||||||
|
let mut crc = crc_any::CRCu8::crc8maxim();
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
|
||||||
|
// Construct packet data
|
||||||
|
&buf[0..2].copy_from_slice(&START_SEQUENCE);
|
||||||
|
buf[2] = command;
|
||||||
|
for i in 0..4 {
|
||||||
|
buf[3 + i] = (value >> (8 * i)) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run CRC and append value
|
||||||
|
crc.digest(&buf[0..7]);
|
||||||
|
buf[7] = crc.get_crc();
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PacketParser {
|
||||||
|
packet_index: u8,
|
||||||
|
crc: crc_any::CRCu8,
|
||||||
|
|
||||||
|
is_write: bool,
|
||||||
|
setting: u8,
|
||||||
|
value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketParser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
packet_index: 0,
|
||||||
|
crc: crc_any::CRCu8::crc8maxim(),
|
||||||
|
|
||||||
|
is_write: false,
|
||||||
|
setting: 0,
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try and parse a packet from a received buffer. Returns Some(packet) if a
|
||||||
|
/// packet was found, and the number of bytes processed from the buffer. Bytes
|
||||||
|
/// processed may be smaller than the buffer length if a packet was found.
|
||||||
|
pub fn try_parse(&mut self, buf: &[u8]) -> (Result<CommandPacket, Error>, usize) {
|
||||||
|
let mut bytes_processed = 0;
|
||||||
|
for byte in buf {
|
||||||
|
bytes_processed += 1;
|
||||||
|
match self.process_byte(*byte) {
|
||||||
|
Ok(packet) => return (Ok(packet), bytes_processed),
|
||||||
|
|
||||||
|
// We don't care to pass errors to the user
|
||||||
|
// TODO log
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(Err(Error::NoPacket), bytes_processed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process incoming byte using a simple state machine.
|
||||||
|
pub fn process_byte(&mut self, byte: u8) -> Result<CommandPacket, Error> {
|
||||||
|
match self.packet_index {
|
||||||
|
// Check the start sequence
|
||||||
|
0 => {
|
||||||
|
if byte != START_SEQUENCE[0] {
|
||||||
|
self.reset();
|
||||||
|
return Err(Error::BadStartSequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
if byte != START_SEQUENCE[1] {
|
||||||
|
self.reset();
|
||||||
|
return Err(Error::BadStartSequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save data
|
||||||
|
2 => {
|
||||||
|
self.is_write = byte & 0x80 != 0;
|
||||||
|
self.setting = byte & 0x7F;
|
||||||
|
}
|
||||||
|
3..=6 => {
|
||||||
|
self.value |= (byte << (8 * (self.packet_index - 3))) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check CRC
|
||||||
|
7 => {
|
||||||
|
if self.crc.get_crc() == byte {
|
||||||
|
return Ok(CommandPacket {
|
||||||
|
is_write: self.is_write,
|
||||||
|
setting: self.setting,
|
||||||
|
value: self.value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.reset();
|
||||||
|
return Err(Error::BadCrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be unreachable, just reset
|
||||||
|
_ => {
|
||||||
|
self.reset();
|
||||||
|
return Err(Error::StateMachineError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment index
|
||||||
|
self.packet_index += 1;
|
||||||
|
// Process CRC (sucks I can't just give it one byte)
|
||||||
|
self.crc.digest(&[byte]);
|
||||||
|
|
||||||
|
Err(Error::NoPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset values to start parsing a packet again
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.packet_index = 0;
|
||||||
|
self.crc = crc_any::CRCu8::crc8maxim();
|
||||||
|
self.value = 0;
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ from litedram.phy import GENSDRPHY, HalfRateGENSDRPHY
|
|||||||
|
|
||||||
from liteeth.phy.ecp5rgmii import LiteEthPHYRGMII
|
from liteeth.phy.ecp5rgmii import LiteEthPHYRGMII
|
||||||
|
|
||||||
from sampler import Sampler
|
from sampler import SamplerController, Sampler
|
||||||
from litex.soc.integration.soc import SoCRegion
|
from litex.soc.integration.soc import SoCRegion
|
||||||
|
|
||||||
from test import run_test, TestResult, skip_suite
|
from test import run_test, TestResult, skip_suite
|
||||||
@ -143,11 +143,11 @@ class BaseSoC(SoCCore):
|
|||||||
if with_video_framebuffer:
|
if with_video_framebuffer:
|
||||||
self.add_video_framebuffer(phy=self.videophy, timings="800x600@60Hz", clock_domain="hdmi")
|
self.add_video_framebuffer(phy=self.videophy, timings="800x600@60Hz", clock_domain="hdmi")
|
||||||
|
|
||||||
self.submodules.sampler = Sampler(platform.request("adc"), self.crg.cd_sample_clock.clk)
|
#samplers = [Sampler(platform.request("adc", i)) for i in range(3)]
|
||||||
sampler_region = SoCRegion(origin=None, size=0x1000, cached=False)
|
#self.submodules.sampler_controller = SamplerController(samplers, buffer_len=2048 * 10)
|
||||||
#self.add_wb_slave(0x9000_0000, self.sampler.bus, 0x1000)
|
### TODO better way to do this?
|
||||||
## TODO better way to do this?
|
#sampler_region = SoCRegion(origin=None, size=0x4000, cached=False)
|
||||||
#self.bus.add_slave(name="sampler", slave=self.sampler.bus, region=sampler_region)
|
#self.bus.add_slave(name="sampler", slave=self.sampler_controller.bus, region=sampler_region)
|
||||||
|
|
||||||
# Build --------------------------------------------------------------------------------------------
|
# Build --------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -185,6 +185,7 @@ def main():
|
|||||||
results.append(run_test("SamplerController", controller.test_bus_access))
|
results.append(run_test("SamplerController", controller.test_bus_access))
|
||||||
results.append(run_test("SamplerController", controller.test_simple_waveform))
|
results.append(run_test("SamplerController", controller.test_simple_waveform))
|
||||||
results.append(run_test("SamplerController", controller.test_simple_waveform_capture_offset))
|
results.append(run_test("SamplerController", controller.test_simple_waveform_capture_offset))
|
||||||
|
results.append(run_test("SamplerController", controller.test_multiple_reads))
|
||||||
results.append(run_test("PeakDetector", peak_detector.test_simple_waveform))
|
results.append(run_test("PeakDetector", peak_detector.test_simple_waveform))
|
||||||
results.append(run_test("PeakDetector", peak_detector.test_scrunched_simple_waveform))
|
results.append(run_test("PeakDetector", peak_detector.test_scrunched_simple_waveform))
|
||||||
results.append(run_test("PeakDetector", peak_detector.test_decay_simple_waveform))
|
results.append(run_test("PeakDetector", peak_detector.test_decay_simple_waveform))
|
||||||
@ -199,8 +200,9 @@ def main():
|
|||||||
|
|
||||||
print(f"{passed}/{passed + failed} passed ({skipped} skipped)")
|
print(f"{passed}/{passed + failed} passed ({skipped} skipped)")
|
||||||
|
|
||||||
# TODO maybe don't do this?
|
if failed > 0 or not args.build:
|
||||||
return
|
# Don't also build after this
|
||||||
|
return
|
||||||
|
|
||||||
# Build firmware
|
# Build firmware
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
@ -19,7 +19,7 @@ from litex.build.lattice.programmer import EcpDapProgrammer
|
|||||||
|
|
||||||
# IOs ----------------------------------------------------------------------------------------------
|
# IOs ----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
_io_v7_0 = [ # Documented by @smunaut
|
_io_v7_0 = [ # Colorlight i9 documented by @smunaut
|
||||||
# Clk
|
# Clk
|
||||||
("clk25", 0, Pins("P3"), IOStandard("LVCMOS33")),
|
("clk25", 0, Pins("P3"), IOStandard("LVCMOS33")),
|
||||||
|
|
||||||
@ -117,13 +117,42 @@ _io_v7_0 = [ # Documented by @smunaut
|
|||||||
|
|
||||||
# High speed parallel ADCs
|
# High speed parallel ADCs
|
||||||
("adc", 0,
|
("adc", 0,
|
||||||
Subsignal("data", Pins("M18 N18 N17 P18 U17 U18 T17 M17 P17 R17")),
|
# Rev A pins
|
||||||
# TODO ???? what other pins are changed in 7.2
|
#Subsignal("data", Pins("M18 N18 N17 P18 U17 U18 T17 M17 P17 R17")),
|
||||||
Subsignal("refclk", Pins("L2")),
|
# Rev B pins
|
||||||
Subsignal("oen_b", Pins("K18")),
|
Subsignal("data", Pins("L20 M18 N18 N17 P18 U17 U18 T17 M17 P17")),
|
||||||
Subsignal("standby", Pins("C18")),
|
Subsignal("refclk", Pins("K18")),
|
||||||
Subsignal("dfs", Pins("T18")),
|
Subsignal("oen_b", Pins("C18")),
|
||||||
Subsignal("otr", Pins("R18")),
|
Subsignal("standby", Pins("T18")),
|
||||||
|
Subsignal("dfs", Pins("R18")),
|
||||||
|
Subsignal("otr", Pins("R17")),
|
||||||
|
IOStandard("LVCMOS33")
|
||||||
|
),
|
||||||
|
("adc", 1,
|
||||||
|
Subsignal("data", Pins("R1 T1 U1 Y2 W1 V1 M1 N2 N3 T2")),
|
||||||
|
Subsignal("refclk", Pins("M4")),
|
||||||
|
Subsignal("oen_b", Pins("N4")),
|
||||||
|
Subsignal("standby", Pins("R3")),
|
||||||
|
Subsignal("dfs", Pins("T3")),
|
||||||
|
Subsignal("otr", Pins("M3")),
|
||||||
|
IOStandard("LVCMOS33")
|
||||||
|
),
|
||||||
|
("adc", 2,
|
||||||
|
Subsignal("data", Pins("A18 C17 A19 B18 B19 B20 C20 D19 D20 E19")),
|
||||||
|
Subsignal("refclk", Pins("J19")),
|
||||||
|
Subsignal("oen_b", Pins("H20")),
|
||||||
|
Subsignal("standby", Pins("G20")),
|
||||||
|
Subsignal("dfs", Pins("G19")),
|
||||||
|
Subsignal("otr", Pins("F20")),
|
||||||
|
IOStandard("LVCMOS33")
|
||||||
|
),
|
||||||
|
("adc", 3,
|
||||||
|
Subsignal("data", Pins("E4 F1 F3 G3 H3 H4 H5 J4 J5 K3")),
|
||||||
|
Subsignal("refclk", Pins("M4")),
|
||||||
|
Subsignal("oen_b", Pins("N4")),
|
||||||
|
Subsignal("standby", Pins("B3")),
|
||||||
|
Subsignal("dfs", Pins("K5")),
|
||||||
|
Subsignal("otr", Pins("K4")),
|
||||||
IOStandard("LVCMOS33")
|
IOStandard("LVCMOS33")
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -146,9 +175,7 @@ _io_v7_2 = copy.deepcopy(_io_v7_0)
|
|||||||
|
|
||||||
for i, x in enumerate(_io_v7_2):
|
for i, x in enumerate(_io_v7_2):
|
||||||
if x[:2] == ("user_led_n", 0):
|
if x[:2] == ("user_led_n", 0):
|
||||||
# TODO fix in HW
|
_io_v7_2[i] = ("user_led_n", 0, Pins("L2"), IOStandard("LVCMOS33"))
|
||||||
#_io_v7_2[i] = ("user_led_n", 0, Pins("L2"), IOStandard("LVCMOS33"))
|
|
||||||
_io_v7_2[i] = ("user_led_n", 0, Pins("J19"), IOStandard("LVCMOS33"))
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# optional, alternative uart location
|
# optional, alternative uart location
|
||||||
|
@ -34,14 +34,23 @@ class CircularBuffer(Module):
|
|||||||
rd_ptr = Signal(ptr_width)
|
rd_ptr = Signal(ptr_width)
|
||||||
empty = Signal(reset=1) # Extra signal to distinguish between full and empty condition
|
empty = Signal(reset=1) # Extra signal to distinguish between full and empty condition
|
||||||
|
|
||||||
|
# TODO this shouldn't be needed. Bug in migen IMO, I don't use this signal
|
||||||
|
dat_r = Signal(width)
|
||||||
|
|
||||||
# Hook write input signals to memory
|
# Hook write input signals to memory
|
||||||
wr_port = storage.get_port(write_capable=True)
|
wr_port = storage.get_port(write_capable=True)
|
||||||
|
# TODO hacky bullshit because migen is broken or I'm using it wrong
|
||||||
|
if not hasattr(wr_port.clock, "cd"):
|
||||||
|
wr_port.clock.cd = "sys"
|
||||||
|
|
||||||
# Always ready to write data into memory, so hook these signals straight in
|
# Always ready to write data into memory, so hook these signals straight in
|
||||||
self.comb += [
|
self.comb += [
|
||||||
wr_port.adr.eq(wr_ptr),
|
wr_port.adr.eq(wr_ptr),
|
||||||
wr_port.dat_w.eq(self.wr_data),
|
wr_port.dat_w.eq(self.wr_data),
|
||||||
wr_port.we.eq(self.wr_valid),
|
wr_port.we.eq(self.wr_valid),
|
||||||
self.wr_ready.eq(1), # We are always ready to write data in
|
self.wr_ready.eq(1), # We are always ready to write data in
|
||||||
|
|
||||||
|
dat_r.eq(wr_port.dat_r)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Advance write (and potentially read)
|
# Advance write (and potentially read)
|
||||||
@ -66,6 +75,10 @@ class CircularBuffer(Module):
|
|||||||
|
|
||||||
# TODO should I actually set async_read?
|
# TODO should I actually set async_read?
|
||||||
rd_port = storage.get_port(async_read=True)
|
rd_port = storage.get_port(async_read=True)
|
||||||
|
# TODO hacky bullshit because migen is broken or I'm using it wrong
|
||||||
|
if not hasattr(rd_port.clock, "cd"):
|
||||||
|
rd_port.clock.cd = "sys"
|
||||||
|
|
||||||
# Set read addr so 0 starts at rd_ptr and wraps around, and connect read data up
|
# Set read addr so 0 starts at rd_ptr and wraps around, and connect read data up
|
||||||
self.comb += [
|
self.comb += [
|
||||||
If(self.rd_addr + rd_ptr < depth,
|
If(self.rd_addr + rd_ptr < depth,
|
||||||
|
@ -27,11 +27,14 @@ class SamplerController(Module):
|
|||||||
|
|
||||||
Registers
|
Registers
|
||||||
--------
|
--------
|
||||||
0x00: Control Register (RW)
|
0x00: Control Register (WO)
|
||||||
Bit 0 - Begin capture. Resets all FIFOs and starts the peak detector
|
Bit 0 - Start capture
|
||||||
|
Bit 1 - Stop capture. Does nothing if capture is not ongoing
|
||||||
|
Bit 2 - Clear sample buffers
|
||||||
|
|
||||||
0x01: Status Register (RO)
|
0x01: Status Register (RO)
|
||||||
Bit 0 - Capture complete. Set by peak detection block and cleared when capture is began
|
Bit 0 - Capture complete. Set by peak detection block and cleared when capture is began
|
||||||
|
Bit 1 - Sampling running
|
||||||
|
|
||||||
0x02: trigger_run_len (RW)
|
0x02: trigger_run_len (RW)
|
||||||
Number of samples to acquire after triggering sample.
|
Number of samples to acquire after triggering sample.
|
||||||
@ -138,7 +141,7 @@ class SamplerController(Module):
|
|||||||
|
|
||||||
# Handle explicit config registers
|
# Handle explicit config registers
|
||||||
cases = {
|
cases = {
|
||||||
0: rw_register(control_register),
|
0: rw_register(control_register, read=False),
|
||||||
1: rw_register(status_register, write=False),
|
1: rw_register(status_register, write=False),
|
||||||
2: rw_register(trigger_run_len),
|
2: rw_register(trigger_run_len),
|
||||||
3: rw_register(self.peak_detector.thresh_value),
|
3: rw_register(self.peak_detector.thresh_value),
|
||||||
@ -156,6 +159,8 @@ class SamplerController(Module):
|
|||||||
# Connect up control registers bus
|
# Connect up control registers bus
|
||||||
self.sync += [
|
self.sync += [
|
||||||
self.control_regs_bus.ack.eq(0),
|
self.control_regs_bus.ack.eq(0),
|
||||||
|
# Hold control register low to use as strobe functionality
|
||||||
|
control_register.eq(0),
|
||||||
If(self.control_regs_bus.cyc & self.control_regs_bus.stb,
|
If(self.control_regs_bus.cyc & self.control_regs_bus.stb,
|
||||||
self.control_regs_bus.ack.eq(1),
|
self.control_regs_bus.ack.eq(1),
|
||||||
Case(self.control_regs_bus.adr, cases)),
|
Case(self.control_regs_bus.adr, cases)),
|
||||||
@ -180,13 +185,20 @@ class SamplerController(Module):
|
|||||||
# We have sampled enough, update status and stop sampling
|
# We have sampled enough, update status and stop sampling
|
||||||
If(post_trigger_count + 1 >= trigger_run_len,
|
If(post_trigger_count + 1 >= trigger_run_len,
|
||||||
status_register[0].eq(1),
|
status_register[0].eq(1),
|
||||||
control_register[0].eq(0))),
|
sample_enable.eq(0))),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Update register storage
|
# Update register storage
|
||||||
self.comb += [
|
self.sync += [
|
||||||
sample_enable.eq(control_register[0]),
|
status_register[1].eq(sample_enable),
|
||||||
|
If(control_register[0], sample_enable.eq(1)),
|
||||||
|
If(control_register[1], sample_enable.eq(0)),
|
||||||
]
|
]
|
||||||
|
for buffer in self.buffers:
|
||||||
|
self.sync += [
|
||||||
|
buffer.clear.eq(0),
|
||||||
|
If(control_register[2], buffer.clear.eq(1)),
|
||||||
|
]
|
||||||
|
|
||||||
def write_wishbone(bus, address, value):
|
def write_wishbone(bus, address, value):
|
||||||
# Set up bus
|
# Set up bus
|
||||||
@ -341,6 +353,11 @@ def test_simple_waveform():
|
|||||||
sample = (yield dut.bus.dat_r)
|
sample = (yield dut.bus.dat_r)
|
||||||
data.append(sample)
|
data.append(sample)
|
||||||
|
|
||||||
|
# Manually validated, this is what we should read on a correct
|
||||||
|
# run
|
||||||
|
assert data[15] == 138
|
||||||
|
assert data[16] == 132
|
||||||
|
|
||||||
# Test pass
|
# Test pass
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -418,3 +435,172 @@ def test_simple_waveform_capture_offset():
|
|||||||
assert False, "We should have triggered"
|
assert False, "We should have triggered"
|
||||||
|
|
||||||
run_simulation(dut, test_fn())
|
run_simulation(dut, test_fn())
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_reads():
|
||||||
|
"""
|
||||||
|
Testing multiple triggers/captures in succession to ensure typical (i.e. repeated) operation
|
||||||
|
works correctly.
|
||||||
|
"""
|
||||||
|
# Enable, trigger works correctly
|
||||||
|
# Enable, tick a bit of data in, should not trigger, and trigger should have reset immediately
|
||||||
|
# Enable again, and tick in lots of data, should trigger again now
|
||||||
|
"""Test a simple waveform captured at an offset"""
|
||||||
|
from .peak_detector import create_waveform
|
||||||
|
_, data = create_waveform()
|
||||||
|
data = [int(d) for d in data]
|
||||||
|
dut = TestSoC(data, buffer_len=32)
|
||||||
|
|
||||||
|
def test_fn():
|
||||||
|
# Set settings
|
||||||
|
yield from write_wishbone(dut.bus, 2, 0) # trigger_run_len = 0
|
||||||
|
yield from write_wishbone(dut.bus, 3, 800) # thresh_value = 800
|
||||||
|
yield from write_wishbone(dut.bus, 4, 10) # thresh_time = 10
|
||||||
|
yield from write_wishbone(dut.bus, 5, 1) # decay_value = 1
|
||||||
|
yield from write_wishbone(dut.bus, 5, 0) # decay_period = 0
|
||||||
|
|
||||||
|
# Start controller
|
||||||
|
yield from write_wishbone(dut.bus, 0, 1)
|
||||||
|
|
||||||
|
triggered_yet = False
|
||||||
|
triggered_num = 0
|
||||||
|
for i in range(1000):
|
||||||
|
(yield dut.samplers[0].index.eq(i))
|
||||||
|
(yield dut.samplers[0].valid.eq(1))
|
||||||
|
yield
|
||||||
|
|
||||||
|
(yield dut.samplers[0].valid.eq(0))
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Total of 6 clocks per sample clock
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
|
||||||
|
if not triggered_yet and (yield dut.controller.peak_detector.triggered) == 1:
|
||||||
|
# Triggered, now we need to run some number of cycles
|
||||||
|
triggered_yet = True
|
||||||
|
|
||||||
|
if triggered_yet:
|
||||||
|
triggered_num += 1
|
||||||
|
if triggered_num > 16:
|
||||||
|
# We should now have collected all our samples
|
||||||
|
yield from read_wishbone(dut.bus, 1)
|
||||||
|
assert (yield dut.bus.dat_r) == 1, "Trigger did not propogate to WB!"
|
||||||
|
|
||||||
|
# Check that length is correct
|
||||||
|
yield from read_wishbone(dut.bus, 0x100)
|
||||||
|
len = (yield dut.bus.dat_r)
|
||||||
|
assert len == 32, f"Len ({len}) not correct!"
|
||||||
|
|
||||||
|
# Read data in
|
||||||
|
data = []
|
||||||
|
for i in range(32):
|
||||||
|
yield from read_wishbone(dut.bus, 0x800 + i)
|
||||||
|
sample = (yield dut.bus.dat_r)
|
||||||
|
data.append(sample)
|
||||||
|
|
||||||
|
|
||||||
|
# Manually validated from test above to be offset into the
|
||||||
|
# data
|
||||||
|
assert data[15] == 138
|
||||||
|
assert data[16] == 132
|
||||||
|
break
|
||||||
|
|
||||||
|
assert triggered_yet, "We should have triggered"
|
||||||
|
|
||||||
|
# Clear out sampler and re-enable
|
||||||
|
yield from write_wishbone(dut.bus, 0, 0b101)
|
||||||
|
yield
|
||||||
|
|
||||||
|
assert (yield dut.controller.peak_detector.triggered) == 0, "Trigger should have been cleared"
|
||||||
|
assert (yield dut.controller.buffers[0].len) == 0, "Buffers should have been cleared"
|
||||||
|
|
||||||
|
# Tick a few clocks through, and we shouldn't have triggered
|
||||||
|
for i in range(10):
|
||||||
|
(yield dut.samplers[0].index.eq(i))
|
||||||
|
(yield dut.samplers[0].valid.eq(1))
|
||||||
|
yield
|
||||||
|
|
||||||
|
(yield dut.samplers[0].valid.eq(0))
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Total of 6 clocks per sample clock
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
|
||||||
|
assert (yield dut.controller.peak_detector.triggered) == 0, "We didn't push enough data through to trigger"
|
||||||
|
|
||||||
|
# Disable sampler, run lots of data through, we should not trigger
|
||||||
|
yield from write_wishbone(dut.bus, 0, 0b010)
|
||||||
|
for i in range(1000):
|
||||||
|
(yield dut.samplers[0].index.eq(i))
|
||||||
|
(yield dut.samplers[0].valid.eq(1))
|
||||||
|
yield
|
||||||
|
|
||||||
|
(yield dut.samplers[0].valid.eq(0))
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Total of 6 clocks per sample clock
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Enable sampler and run again, we should get another trigger
|
||||||
|
yield from write_wishbone(dut.bus, 2, 16) # trigger_run_len = 16
|
||||||
|
yield from write_wishbone(dut.bus, 0, 1)
|
||||||
|
triggered_yet = False
|
||||||
|
triggered_num = 0
|
||||||
|
for i in range(1000):
|
||||||
|
(yield dut.samplers[0].index.eq(i))
|
||||||
|
(yield dut.samplers[0].valid.eq(1))
|
||||||
|
yield
|
||||||
|
|
||||||
|
(yield dut.samplers[0].valid.eq(0))
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Total of 6 clocks per sample clock
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
yield
|
||||||
|
|
||||||
|
if not triggered_yet and (yield dut.controller.peak_detector.triggered) == 1:
|
||||||
|
# Triggered, now we need to run some number of cycles
|
||||||
|
triggered_yet = True
|
||||||
|
|
||||||
|
if triggered_yet:
|
||||||
|
triggered_num += 1
|
||||||
|
if triggered_num > 16:
|
||||||
|
# We should now have collected all our samples
|
||||||
|
yield from read_wishbone(dut.bus, 1)
|
||||||
|
assert (yield dut.bus.dat_r) == 1, "Trigger did not propogate to WB!"
|
||||||
|
|
||||||
|
# Check that length is correct
|
||||||
|
yield from read_wishbone(dut.bus, 0x100)
|
||||||
|
len = (yield dut.bus.dat_r)
|
||||||
|
assert len == 32, f"Len ({len}) not correct!"
|
||||||
|
|
||||||
|
# Read data in
|
||||||
|
data = []
|
||||||
|
for i in range(32):
|
||||||
|
yield from read_wishbone(dut.bus, 0x800 + i)
|
||||||
|
sample = (yield dut.bus.dat_r)
|
||||||
|
data.append(sample)
|
||||||
|
|
||||||
|
|
||||||
|
# Manually validated from test above to be offset into the
|
||||||
|
# data
|
||||||
|
assert data[0] == 138
|
||||||
|
assert data[1] == 132
|
||||||
|
|
||||||
|
# Test pass
|
||||||
|
return
|
||||||
|
|
||||||
|
assert triggered_yet, "We should have triggered"
|
||||||
|
|
||||||
|
run_simulation(dut, test_fn(), vcd_name="controller.vcd")
|
||||||
|
@ -3,14 +3,9 @@ from migen.genlib.cdc import PulseSynchronizer
|
|||||||
|
|
||||||
|
|
||||||
class Sampler(Module):
|
class Sampler(Module):
|
||||||
def __init__(self, adc_pins: Record, sampler_clock: Signal):
|
def __init__(self, adc_pins: Record):
|
||||||
# self.clock_domains.foo = ClockDomain() is how to add a new clock domain, accessible at self.foo
|
|
||||||
# Connect sampler clock domain
|
|
||||||
self.clock_domains.sample_clock = ClockDomain("sample_clock")
|
|
||||||
self.comb += self.sample_clock.clk.eq(sampler_clock)
|
|
||||||
|
|
||||||
# Hook up ADC REFCLK to sample_clock
|
# Hook up ADC REFCLK to sample_clock
|
||||||
self.comb += adc_pins.refclk.eq(sampler_clock)
|
self.comb += adc_pins.refclk.eq(ClockDomain("sample_clock").clk)
|
||||||
|
|
||||||
# We can synchronize to the sampler clock, whenever it goes high we can
|
# We can synchronize to the sampler clock, whenever it goes high we can
|
||||||
# strobe a single valid signal
|
# strobe a single valid signal
|
||||||
@ -21,10 +16,10 @@ class Sampler(Module):
|
|||||||
self.data = Signal(10)
|
self.data = Signal(10)
|
||||||
|
|
||||||
self.comb += [
|
self.comb += [
|
||||||
synchronizer.i.eq(self.sample_clock.clk),
|
synchronizer.i.eq(ClockDomain("sample_clock").clk),
|
||||||
self.valid.eq(synchronizer.o),
|
self.valid.eq(synchronizer.o),
|
||||||
self.data.eq(adc_pins.data),
|
|
||||||
]
|
]
|
||||||
|
self.sync += self.data.eq(adc_pins.data)
|
||||||
|
|
||||||
# Set config pins to constant values
|
# Set config pins to constant values
|
||||||
self.comb += adc_pins.oen_b.eq(0) # Data pins enable
|
self.comb += adc_pins.oen_b.eq(0) # Data pins enable
|
||||||
|
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Built from this documentation: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pysonar"
|
||||||
|
description = "Library to communicate with ARVP's sonar FPGA"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{ name = "David Lenfesty", email = "lenfesty@ualberta.ca" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["crc>=4.2"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
# Export CLI to configure FPGA
|
||||||
|
sonar_config = "pysonar:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "setuptools-scm"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
# Just include the python directory
|
||||||
|
packages = ["pysonar"]
|
2
pysonar/__init__.py
Normal file
2
pysonar/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello world!")
|
108
pysonar/command_packets.py
Normal file
108
pysonar/command_packets.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
from crc import Calculator, Crc8
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import IntEnum
|
||||||
|
from struct import pack
|
||||||
|
|
||||||
|
|
||||||
|
PKT_START_SEQUENCE = [0xDE, 0xAD]
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(IntEnum):
|
||||||
|
TriggerThreshold = 0
|
||||||
|
TriggerPeriod = 1
|
||||||
|
DecayValue = 2
|
||||||
|
DecayPeriod = 3
|
||||||
|
Gain = 4
|
||||||
|
CenterFreq = 5
|
||||||
|
SamplingEnabled = 6
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandPacket:
|
||||||
|
is_write: bool
|
||||||
|
setting: Settings
|
||||||
|
value: int
|
||||||
|
|
||||||
|
def serialize(self) -> bytearray:
|
||||||
|
buf = bytearray()
|
||||||
|
|
||||||
|
# TODO raise exception if setting is invalid here
|
||||||
|
buf.extend(PKT_START_SEQUENCE)
|
||||||
|
command = int(self.setting)
|
||||||
|
if self.is_write:
|
||||||
|
command |= 1 << 7
|
||||||
|
buf.append(command)
|
||||||
|
buf.extend(pack("<I", self.value))
|
||||||
|
|
||||||
|
# Append CRC
|
||||||
|
crc = Calculator(Crc8.MAXIM_DOW).checksum(buf)
|
||||||
|
buf.append(crc)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResponsePacket:
|
||||||
|
# Deserialization
|
||||||
|
is_error: bool
|
||||||
|
setting: Settings
|
||||||
|
value: int
|
||||||
|
|
||||||
|
|
||||||
|
class PacketParser:
|
||||||
|
def __init__(self):
|
||||||
|
self.crc = Calculator(Crc8.MAXIM_DOW)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_byte(self, b: int) -> Optional[ResponsePacket]:
|
||||||
|
byte_index = len(self.packet_data)
|
||||||
|
|
||||||
|
if byte_index == 0:
|
||||||
|
if b != PKT_START_SEQUENCE[0]:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
elif byte_index == 1:
|
||||||
|
if b != PKT_START_SEQUENCE[1]:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
elif byte_index == 7:
|
||||||
|
# Final CRC byte, check and generate data
|
||||||
|
packet = None
|
||||||
|
if self.crc.verify(self.packet_data, b):
|
||||||
|
packet = self.build_response_packet()
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
return packet
|
||||||
|
|
||||||
|
# Pull packet data in otherwise
|
||||||
|
self.packet_data.append(b)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bytearray(self, bytes: bytearray) -> Tuple[bytearray, Optional[ResponsePacket]]:
|
||||||
|
"""
|
||||||
|
Parse a bytearray for a packet. Returns a bytearray of bytes that haven't been processed.
|
||||||
|
"""
|
||||||
|
for offset, byte in enumerate(bytes):
|
||||||
|
packet = self.parse_byte(byte)
|
||||||
|
|
||||||
|
if packet is not None:
|
||||||
|
# Return packet and consumed bytearray
|
||||||
|
return (bytes[offset:], packet)
|
||||||
|
|
||||||
|
# No packets found, entire buffer has been read
|
||||||
|
return (bytearray(), None)
|
||||||
|
|
||||||
|
|
||||||
|
def build_response_packet(self) -> ResponsePacket:
|
||||||
|
"""Builds a response packet out of the data we have received"""
|
||||||
|
if len(self.packet_data) < 7:
|
||||||
|
raise Exception("Invalid amount of packet data received!")
|
||||||
|
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.packet_data = bytearray()
|
Loading…
Reference in New Issue
Block a user