diff --git a/README.md b/README.md index 094462f..4fe4ae1 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ Set report mode to `on` for a continuous stream of input data. The scope of this setting is per TCP session. -### Commands +### TCP commands + +Send commands as simple text string terminated by `\n`. Responses are +formatted as line-delimited JSON. | Syntax | Function | | --- | --- | diff --git a/pytec/example.py b/pytec/example.py index b519532..001ea49 100644 --- a/pytec/example.py +++ b/pytec/example.py @@ -2,7 +2,10 @@ from pytec.client import Client tec = Client() #(host="localhost", port=6667) tec.set_param("s-h", 1, "t0", 20) +print(tec.get_pwm()) print(tec.get_pid()) +print(tec.get_pwm()) +print(tec.get_postfilter()) print(tec.get_steinhart_hart()) for data in tec.report_mode(): print(data) diff --git a/pytec/plot.py b/pytec/plot.py index 951d757..600fcda 100644 --- a/pytec/plot.py +++ b/pytec/plot.py @@ -39,21 +39,29 @@ series = { 'i_tec': Series(), 'tec_i': Series(), 'tec_u_meas': Series(), + 'interval': Series(), } series_lock = Lock() quit = False +last_packet_time = None def recv_data(tec): + global last_packet_time for data in tec.report_mode(): if data['channel'] == 0: series_lock.acquire() try: time = data['time'] / 1000.0 + if last_packet_time: + data['interval'] = time - last_packet_time + last_packet_time = time + for k, s in series.items(): - v = data[k] - if k in data and type(v) is float: - s.append(time, v) + if k in data: + v = data[k] + if type(v) is float: + s.append(time, v) finally: series_lock.release() @@ -108,9 +116,11 @@ def animate(i): finally: series_lock.release() - margin_y = 0.01 * (max_y - min_y) - ax.set_xlim(min_x, max_x) - ax.set_ylim(min_y - margin_y, max_y + margin_y) + if min_x != max_x: + ax.set_xlim(min_x, max_x) + if min_y != max_y: + margin_y = 0.01 * (max_y - min_y) + ax.set_ylim(min_y - margin_y, max_y + margin_y) global legend legend.remove() diff --git a/pytec/pytec/client.py b/pytec/pytec/client.py index 0098386..32f1bef 100644 --- a/pytec/pytec/client.py +++ b/pytec/pytec/client.py @@ -1,16 +1,14 @@ import socket import json -CHANNELS = 2 +class CommandError(Exception): + pass class Client: def __init__(self, host="192.168.1.26", port=23, timeout=None): self._socket = socket.create_connection((host, port), timeout) self._lines = [""] - def _command(self, *command): - self._socket.sendall((" ".join(command) + "\n").encode('utf-8')) - def _read_line(self): # read more lines while len(self._lines) <= 1: @@ -24,13 +22,19 @@ class Client: self._lines = self._lines[1:] return line + def _command(self, *command): + self._socket.sendall((" ".join(command) + "\n").encode('utf-8')) + + line = self._read_line() + response = json.loads(line) + if "error" in response: + raise CommandError(response["error"]) + return response + def _get_conf(self, topic): - self._command(topic) - result = [] - for channel in range(0, CHANNELS): - line = self._read_line() - conf = json.loads(line) - result.append(conf) + result = [None, None] + for item in self._command(topic): + result[int(item["channel"])] = item return result def get_pwm(self): diff --git a/src/channels.rs b/src/channels.rs index 24d3920..c1d4584 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,3 +1,4 @@ +use heapless::{consts::{U2, U1024}, Vec}; use serde::{Serialize, Serializer}; use smoltcp::time::Instant; use stm32f4xx_hal::hal; @@ -412,7 +413,7 @@ impl Channels { (duty * max, max) } - pub fn report(&mut self, channel: usize) -> Report { + fn report(&mut self, channel: usize) -> Report { let vref = self.channel_state(channel).vref; let (i_set, _) = self.get_i(channel); let i_tec = self.read_itec(channel); @@ -441,7 +442,23 @@ impl Channels { } } - pub fn pwm_summary(&mut self, channel: usize) -> PwmSummary { + pub fn reports_json(&mut self) -> Result { + let mut reports = Vec::<_, U2>::new(); + for channel in 0..CHANNELS { + let _ = reports.push(self.report(channel)); + } + serde_json_core::to_vec(&reports) + } + + pub fn pid_summaries_json(&mut self) -> Result { + let mut summaries = Vec::<_, U2>::new(); + for channel in 0..CHANNELS { + let _ = summaries.push(self.channel_state(channel).pid.summary(channel)); + } + serde_json_core::to_vec(&summaries) + } + + fn pwm_summary(&mut self, channel: usize) -> PwmSummary { PwmSummary { channel, center: CenterPointJson(self.channel_state(channel).center.clone()), @@ -452,19 +469,43 @@ impl Channels { } } - pub fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary { + pub fn pwm_summaries_json(&mut self) -> Result { + let mut summaries = Vec::<_, U2>::new(); + for channel in 0..CHANNELS { + let _ = summaries.push(self.pwm_summary(channel)); + } + serde_json_core::to_vec(&summaries) + } + + fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary { let rate = self.adc.get_postfilter(channel as u8).unwrap() .and_then(|filter| filter.output_rate()); PostFilterSummary { channel, rate } } - pub fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary { + pub fn postfilter_summaries_json(&mut self) -> Result { + let mut summaries = Vec::<_, U2>::new(); + for channel in 0..CHANNELS { + let _ = summaries.push(self.postfilter_summary(channel)); + } + serde_json_core::to_vec(&summaries) + } + + fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary { let params = self.channel_state(channel).sh.clone(); SteinhartHartSummary { channel, params } } + + pub fn steinhart_hart_summaries_json(&mut self) -> Result { + let mut summaries = Vec::<_, U2>::new(); + for channel in 0..CHANNELS { + let _ = summaries.push(self.steinhart_hart_summary(channel)); + } + serde_json_core::to_vec(&summaries) + } } -type JsonBuffer = heapless::Vec; +type JsonBuffer = Vec; #[derive(Serialize)] pub struct Report { @@ -484,12 +525,6 @@ pub struct Report { pid_output: Option, } -impl Report { - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } -} - pub struct CenterPointJson(CenterPoint); // used in JSON encoding, not for config @@ -529,91 +564,14 @@ pub struct PwmSummary { max_i_neg: PwmSummaryField, } -impl PwmSummary { - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } -} - #[derive(Serialize)] pub struct PostFilterSummary { channel: usize, rate: Option, } -impl PostFilterSummary { - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } -} - #[derive(Serialize)] pub struct SteinhartHartSummary { channel: usize, params: steinhart_hart::Parameters, } - -impl SteinhartHartSummary { - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn report_to_json() { - // `/ 1.1` results in values with a really long serialization - let report = Report { - channel: 0, - time: 3200, - adc: Some(ElectricPotential::new::(0.65 / 1.1)), - sens: Some(ElectricalResistance::new::(10000.0 / 1.1)), - temperature: Some(30.0 / 1.1), - pid_engaged: false, - i_set: ElectricCurrent::new::(0.5 / 1.1), - vref: ElectricPotential::new::(1.5 / 1.1), - dac_value: ElectricPotential::new::(2.0 / 1.1), - dac_feedback: ElectricPotential::new::(2.0 / 1.1), - i_tec: ElectricPotential::new::(2.0 / 1.1), - tec_i: ElectricCurrent::new::(0.2 / 1.1), - tec_u_meas: ElectricPotential::new::(2.0 / 1.1), - pid_output: Some(ElectricCurrent::new::(0.5 / 1.1)), - }; - let buf = report.to_json().unwrap(); - assert_eq!(buf[0], b'{'); - assert_eq!(buf[buf.len() - 1], b'}'); - } - - #[test] - fn pwm_summary_to_json() { - let value = 1.0 / 1.1; - let max = 5.0 / 1.1; - - let pwm_summary = PwmSummary { - channel: 0, - center: CenterPointJson(CenterPoint::Vref), - i_set: PwmSummaryField { - value: ElectricCurrent::new::(value), - max: ElectricCurrent::new::(max), - }, - max_v: PwmSummaryField { - value: ElectricPotential::new::(value), - max: ElectricPotential::new::(max), - }, - max_i_pos: PwmSummaryField { - value: ElectricCurrent::new::(value), - max: ElectricCurrent::new::(max), - }, - max_i_neg: PwmSummaryField { - value: ElectricCurrent::new::(value), - max: ElectricCurrent::new::(max), - }, - }; - let buf = pwm_summary.to_json().unwrap(); - assert_eq!(buf[0], b'{'); - assert_eq!(buf[buf.len() - 1], b'}'); - } -} diff --git a/src/main.rs b/src/main.rs index 340799a..41b878f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,7 +81,8 @@ const TCP_PORT: u16 = 23; fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool { let send_free = socket.send_capacity() - socket.send_queue(); if data.len() > send_free + 1 { - // Not enough buffer space, skip report for now + // Not enough buffer space, skip report for now, + // instead of sending incomplete line warn!( "TCP socket has only {}/{} needed {}", send_free + 1, socket.send_capacity(), data.len(), @@ -103,17 +104,6 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool { false } -fn report_to(channel: usize, channels: &mut Channels, socket: &mut TcpSocket) -> bool { - match channels.report(channel).to_json() { - Ok(buf) => - send_line(socket, &buf[..]), - Err(e) => { - error!("unable to serialize report: {:?}", e); - false - } - } -} - /// Initialization and main loop #[cfg(not(test))] #[entry] @@ -221,68 +211,79 @@ fn main() -> ! { socket.close() } else if socket.can_send() && socket.can_recv() { match socket.recv(|buf| session.feed(buf)) { - Ok(SessionInput::Nothing) => {} + Ok(SessionInput::Nothing) => { + send_line(&mut socket, b"{}"); + } Ok(SessionInput::Command(command)) => match command { Command::Quit => socket.close(), Command::Reporting(_reporting) => { // handled by session + send_line(&mut socket, b"{}"); } Command::Show(ShowCommand::Reporting) => { let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting()); } Command::Show(ShowCommand::Input) => { - for channel in 0..CHANNELS { - report_to(channel, &mut channels, &mut socket); + match channels.reports_json() { + Ok(buf) => { + send_line(&mut socket, &buf[..]); + } + Err(e) => { + error!("unable to serialize report: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); + + } } } Command::Show(ShowCommand::Pid) => { - for channel in 0..CHANNELS { - match channels.channel_state(channel).pid.summary(channel).to_json() { - Ok(buf) => { - send_line(&mut socket, &buf); - } - Err(e) => - error!("unable to serialize pid summary: {:?}", e), + match channels.pid_summaries_json() { + Ok(buf) => { + send_line(&mut socket, &buf); + } + Err(e) => { + error!("unable to serialize pid summary: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); } } } Command::Show(ShowCommand::Pwm) => { - for channel in 0..CHANNELS { - match channels.pwm_summary(channel).to_json() { - Ok(buf) => { - send_line(&mut socket, &buf); - } - Err(e) => - error!("unable to serialize pwm summary: {:?}", e), + match channels.pwm_summaries_json() { + Ok(buf) => { + send_line(&mut socket, &buf); + } + Err(e) => { + error!("unable to serialize pwm summary: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); } } } Command::Show(ShowCommand::SteinhartHart) => { - for channel in 0..CHANNELS { - match channels.steinhart_hart_summary(channel).to_json() { - Ok(buf) => { - send_line(&mut socket, &buf); - } - Err(e) => - error!("unable to serialize steinhart-hart summary: {:?}", e), + match channels.steinhart_hart_summaries_json() { + Ok(buf) => { + send_line(&mut socket, &buf); + } + Err(e) => { + error!("unable to serialize steinhart-hart summaries: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); } } } Command::Show(ShowCommand::PostFilter) => { - for channel in 0..CHANNELS { - match channels.postfilter_summary(channel).to_json() { - Ok(buf) => { - send_line(&mut socket, &buf); - } - Err(e) => - error!("unable to serialize postfilter summary: {:?}", e), + match channels.postfilter_summaries_json() { + Ok(buf) => { + send_line(&mut socket, &buf); + } + Err(e) => { + error!("unable to serialize postfilter summary: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); } } } Command::PwmPid { channel } => { channels.channel_state(channel).pid_engaged = true; leds.g3.on(); + send_line(&mut socket, b"{}"); } Command::Pwm { channel, pin, value } => { match pin { @@ -306,6 +307,7 @@ fn main() -> ! { channels.set_max_i_neg(channel, current); } } + send_line(&mut socket, b"{}"); } Command::CenterPoint { channel, center } => { let (i_tec, _) = channels.get_i(channel); @@ -314,6 +316,7 @@ fn main() -> ! { if !state.pid_engaged { channels.set_i(channel, i_tec); } + send_line(&mut socket, b"{}"); } Command::Pid { channel, parameter, value } => { let pid = &mut channels.channel_state(channel).pid; @@ -336,6 +339,7 @@ fn main() -> ! { IntegralMax => pid.parameters.integral_max = value as f32, } + send_line(&mut socket, b"{}"); } Command::SteinhartHart { channel, parameter, value } => { let sh = &mut channels.channel_state(channel).sh; @@ -345,29 +349,41 @@ fn main() -> ! { B => sh.b = value, R0 => sh.r0 = ElectricalResistance::new::(value), } + send_line(&mut socket, b"{}"); } Command::PostFilter { channel, rate: None } => { channels.adc.set_postfilter(channel as u8, None).unwrap(); + send_line(&mut socket, b"{}"); } Command::PostFilter { channel, rate: Some(rate) } => { let filter = ad7172::PostFilter::closest(rate); match filter { - Some(filter) => - channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(), - None => - error!("unable to choose postfilter for rate {:.3}", rate), + Some(filter) => { + channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(); + send_line(&mut socket, b"{}"); + } + None => { + error!("unable to choose postfilter for rate {:.3}", rate); + send_line(&mut socket, b"{{\"error\": \"unable to choose postfilter rate\"}}"); + } } } Command::Load { channel } => { for c in 0..CHANNELS { if channel.is_none() || channel == Some(c) { match store.read_value::(CHANNEL_CONFIG_KEY[c]) { - Ok(Some(config)) => - config.apply(&mut channels, c), - Ok(None) => - error!("flash config not found"), - Err(e) => - error!("unable to load config from flash: {:?}", e), + Ok(Some(config)) => { + config.apply(&mut channels, c); + send_line(&mut socket, b"{}"); + } + Ok(None) => { + error!("flash config not found"); + send_line(&mut socket, b"{{\"error\": \"flash config not found\"}}"); + } + Err(e) => { + error!("unable to load config from flash: {:?}", e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); + } } } } @@ -376,9 +392,13 @@ fn main() -> ! { for c in 0..CHANNELS { if channel.is_none() || channel == Some(c) { let config = ChannelConfig::new(&mut channels, c); - let _ = store - .write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) - .map_err(|e| error!("unable to save channel {} config to flash: {:?}", c, e)); + match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) { + Ok(()) => {} + Err(e) => { + error!("unable to save channel {} config to flash: {:?}", c, e); + let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e); + } + } } } } @@ -387,6 +407,7 @@ fn main() -> ! { .write_value("ipv4", &config, [0; 16]) .map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e)); new_ipv4_config = Some(config); + send_line(&mut socket, b"{}"); } Command::Reset => { for i in 0..CHANNELS { @@ -405,8 +426,15 @@ fn main() -> ! { } } else if socket.can_send() { if let Some(channel) = session.is_report_pending() { - if report_to(channel, &mut channels, &mut socket) { - session.mark_report_sent(channel); + match channels.reports_json() { + Ok(buf) => { + send_line(&mut socket, &buf[..]); + session.mark_report_sent(channel); + } + Err(e) => { + error!("unable to serialize report: {:?}", e); + + } } } } diff --git a/src/pid.rs b/src/pid.rs index b9a706a..461e60a 100644 --- a/src/pid.rs +++ b/src/pid.rs @@ -91,8 +91,6 @@ impl Controller { } } -type JsonBuffer = heapless::Vec; - #[derive(Clone, Serialize, Deserialize)] pub struct Summary { channel: usize, @@ -101,12 +99,6 @@ pub struct Summary { integral: f64, } -impl Summary { - pub fn to_json(&self) -> Result { - serde_json_core::to_vec(self) - } -} - #[cfg(test)] mod test { use super::*;