mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-13 07:48:30 +00:00
requires the feature "nettest" to be present in shellchat. this commit has it turned on, it'll be stripped out before we push to main.
476 lines
No EOL
18 KiB
Rust
476 lines
No EOL
18 KiB
Rust
#![cfg_attr(target_os = "none", no_std)]
|
|
#![cfg_attr(target_os = "none", no_main)]
|
|
|
|
/*! Detailed docs are parked under Modules/cmds "Shell Chat" below
|
|
|
|
To make your own command, copy the `ver.rs` template (for an example with argument parsing),
|
|
or snag the very simple echo template below, and put
|
|
it in the services/shellchat/src/cmds/ directory:
|
|
|
|
```Rust
|
|
use crate::{ShellCmdApi, CommonEnv};
|
|
use xous_ipc::String;
|
|
|
|
#[derive(Debug)]
|
|
pub struct Echo {
|
|
}
|
|
|
|
impl<'a> ShellCmdApi<'a> for Echo {
|
|
cmd_api!(echo); // inserts boilerplate for command API
|
|
|
|
fn process(&mut self, args: String::<1024>, _env: &mut CommonEnv) -> Result<Option<String::<1024>>, xous::Error> {
|
|
Ok(Some(rest))
|
|
}
|
|
}
|
|
```
|
|
|
|
Once you've added your command to the directory, go to the `cmds.rs` file, and follow
|
|
the four-step instructions embedded within the file, starting around line 40.
|
|
|
|
Check for more detailed docs under Modules/cmds "Shell Chat" below
|
|
*/
|
|
use log::info;
|
|
|
|
use core::fmt::Write;
|
|
|
|
use gam::UxRegistration;
|
|
use graphics_server::{Gid, Point, Rectangle, TextBounds, TextView, DrawStyle, PixelColor};
|
|
use graphics_server::api::GlyphStyle;
|
|
use xous::MessageEnvelope;
|
|
use xous_ipc::Buffer;
|
|
|
|
#[doc = include_str!("../README.md")]
|
|
mod cmds;
|
|
use cmds::*;
|
|
|
|
mod oqc_test;
|
|
|
|
#[cfg(feature="nettest")]
|
|
mod nettests;
|
|
|
|
#[cfg(feature="tts")]
|
|
use locales::t;
|
|
#[cfg(feature="tts")]
|
|
use tts_frontend::*;
|
|
|
|
#[cfg(feature="tracking-alloc")]
|
|
use tracking_allocator::{
|
|
AllocationGroupId, AllocationTracker, Allocator,
|
|
};
|
|
#[cfg(feature="tracking-alloc")]
|
|
use std::alloc::System;
|
|
#[cfg(feature="tracking-alloc")]
|
|
#[global_allocator]
|
|
static GLOBAL: Allocator<System> = Allocator::system();
|
|
#[cfg(feature="tracking-alloc")]
|
|
use core::sync::atomic::{AtomicIsize, Ordering};
|
|
#[cfg(feature="tracking-alloc")]
|
|
struct StdoutTracker {
|
|
pub total: AtomicIsize,
|
|
}
|
|
|
|
#[cfg(feature="tracking-alloc")]
|
|
impl AllocationTracker for StdoutTracker {
|
|
fn allocated(&self, addr: usize, size: usize, group_id: AllocationGroupId) {
|
|
// Allocations have all the pertinent information upfront, which you may or may not want to store for further
|
|
// analysis. Notably, deallocations also know how large they are, and what group ID they came from, so you
|
|
// typically don't have to store much data for correlating deallocations with their original allocation.
|
|
self.total.store(self.total.load(Ordering::SeqCst) + size as isize, Ordering::SeqCst);
|
|
println!(
|
|
"allocation -> total={} addr=0x{:0x} size={} group_id={:?}",
|
|
self.total.load(Ordering::SeqCst), addr, size, group_id
|
|
);
|
|
}
|
|
|
|
fn deallocated(
|
|
&self,
|
|
addr: usize,
|
|
size: usize,
|
|
source_group_id: AllocationGroupId,
|
|
current_group_id: AllocationGroupId,
|
|
) {
|
|
// When a deallocation occurs, as mentioned above, you have full access to the address, size of the allocation,
|
|
// as well as the group ID the allocation was made under _and_ the active allocation group ID.
|
|
//
|
|
// This can be useful beyond just the obvious "track how many current bytes are allocated by the group", instead
|
|
// going further to see the chain of where allocations end up, and so on.
|
|
self.total.store(self.total.load(Ordering::SeqCst) - size as isize, Ordering::SeqCst);
|
|
println!(
|
|
"deallocation -> total={} addr=0x{:0x} size={} source_group_id={:?} current_group_id={:?}",
|
|
self.total.load(Ordering::SeqCst), addr, size, source_group_id, current_group_id
|
|
);
|
|
}
|
|
}
|
|
#[derive(Debug)]
|
|
struct History {
|
|
// the history record
|
|
pub text: String,
|
|
// if true, this was input from the user; if false, it's a response from the shell
|
|
pub is_input: bool,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
struct Repl {
|
|
// optional structures that indicate new input to the Repl loop per iteration
|
|
// an input string
|
|
input: Option<String>,
|
|
// messages from other servers
|
|
msg: Option<MessageEnvelope>,
|
|
|
|
// record our input history
|
|
history: Vec::<History>,
|
|
history_len: usize,
|
|
content: Gid,
|
|
gam: gam::Gam,
|
|
|
|
// variables that define our graphical attributes
|
|
screensize: Point,
|
|
bubble_width: u16,
|
|
margin: Point, // margin to edge of canvas
|
|
bubble_margin: Point, // margin of text in bubbles
|
|
bubble_radius: u16,
|
|
bubble_space: i16, // spacing between text bubbles
|
|
|
|
// command environment
|
|
env: CmdEnv,
|
|
|
|
// our security token for making changes to our record on the GAM
|
|
token: [u32; 4],
|
|
#[cfg(feature="tts")]
|
|
tts: TtsFrontend,
|
|
}
|
|
impl Repl{
|
|
fn new(xns: &xous_names::XousNames, sid: xous::SID) -> Self {
|
|
let gam = gam::Gam::new(xns).expect("can't connect to GAM");
|
|
|
|
let token = gam.register_ux(UxRegistration {
|
|
app_name: xous_ipc::String::<128>::from_str(gam::APP_NAME_SHELLCHAT),
|
|
ux_type: gam::UxType::Chat,
|
|
#[cfg(not(feature="tts"))]
|
|
predictor: Some(xous_ipc::String::<64>::from_str(ime_plugin_shell::SERVER_NAME_IME_PLUGIN_SHELL)),
|
|
#[cfg(feature="tts")]
|
|
predictor: Some(xous_ipc::String::<64>::from_str(ime_plugin_tts::SERVER_NAME_IME_PLUGIN_TTS)),
|
|
listener: sid.to_array(), // note disclosure of our SID to the GAM -- the secret is now shared with the GAM!
|
|
redraw_id: ShellOpcode::Redraw.to_u32().unwrap(),
|
|
gotinput_id: Some(ShellOpcode::Line.to_u32().unwrap()),
|
|
audioframe_id: None,
|
|
rawkeys_id: None,
|
|
focuschange_id: Some(ShellOpcode::ChangeFocus.to_u32().unwrap()),
|
|
}).expect("couldn't register Ux context for shellchat");
|
|
|
|
let content = gam.request_content_canvas(token.unwrap()).expect("couldn't get content canvas");
|
|
log::trace!("content canvas {:?}", content);
|
|
let screensize = gam.get_canvas_bounds(content).expect("couldn't get dimensions of content canvas");
|
|
log::trace!("size {:?}", screensize);
|
|
Repl {
|
|
input: None,
|
|
msg: None,
|
|
history: Vec::new(),
|
|
history_len: 10,
|
|
content,
|
|
gam,
|
|
screensize,
|
|
bubble_width: ((screensize.x / 5) * 4) as u16, // 80% width for the text bubbles
|
|
margin: Point::new(4, 4),
|
|
bubble_margin: Point::new(4, 4),
|
|
bubble_radius: 4,
|
|
bubble_space: 4,
|
|
env: CmdEnv::new(xns),
|
|
token: token.unwrap(),
|
|
#[cfg(feature="tts")]
|
|
tts: TtsFrontend::new(xns).unwrap(),
|
|
}
|
|
}
|
|
|
|
/// accept a new input string
|
|
fn input(&mut self, line: &str) -> Result<(), xous::Error> {
|
|
self.input = Some(String::from(line));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn msg(&mut self, message: MessageEnvelope) {
|
|
self.msg = Some(message);
|
|
}
|
|
|
|
fn circular_push(&mut self, item: History) {
|
|
if self.history.len() >= self.history_len {
|
|
self.history.remove(0);
|
|
}
|
|
self.history.push(item);
|
|
}
|
|
|
|
/// update the loop, in response to various inputs
|
|
fn update(&mut self, was_callback: bool) -> Result<(), xous::Error> {
|
|
let debug1 = false;
|
|
// if we had an input string, do something
|
|
if let Some(local) = &self.input {
|
|
let input_history = History {
|
|
text: local.to_string(),
|
|
is_input: true,
|
|
};
|
|
self.circular_push(input_history);
|
|
}
|
|
|
|
// AT THIS POINT: if we have other inputs, update accordingly
|
|
// other inputs might be, for example, events that came in from other servers that would
|
|
// side effect our commands
|
|
|
|
// redraw UI once upon accepting all input
|
|
if !was_callback { // don't need to redraw on a callback, save some cycles
|
|
self.redraw().expect("can't redraw");
|
|
}
|
|
|
|
let mut dirty = true;
|
|
// take the input and pass it on to the various command parsers, and attach result
|
|
if let Some(local) = &self.input {
|
|
log::trace!("processing line: {}", local);
|
|
if let Some(res) = self.env.dispatch(Some(&mut xous_ipc::String::<1024>::from_str(&local)), None).expect("command dispatch failed") {
|
|
#[cfg(feature="tts")]
|
|
{
|
|
let mut output = t!("shellchat.output-tts", xous::LANG).to_string();
|
|
output.push_str(res.as_str().unwrap_or("UTF-8 error"));
|
|
self.tts.tts_simple(&output).unwrap();
|
|
}
|
|
let output_history = History {
|
|
text: String::from(res.as_str().unwrap_or("UTF-8 Error")),
|
|
is_input: false
|
|
};
|
|
self.circular_push(output_history);
|
|
} else {
|
|
dirty = false;
|
|
}
|
|
} else if let Some(msg) = &self.msg {
|
|
log::trace!("processing callback msg: {:?}", msg);
|
|
if let Some(res) = self.env.dispatch(None, Some(msg)).expect("callback failed") {
|
|
#[cfg(feature="tts")]
|
|
{
|
|
let mut output = t!("shellchat.output-tts", xous::LANG).to_string();
|
|
output.push_str(res.as_str().unwrap_or("UTF-8 error"));
|
|
self.tts.tts_simple(&output).unwrap();
|
|
}
|
|
let output_history = History {
|
|
text: String::from(res.as_str().unwrap_or("UTF-8 Error")),
|
|
is_input: false
|
|
};
|
|
self.circular_push(output_history);
|
|
} else {
|
|
dirty = false;
|
|
}
|
|
}
|
|
|
|
// clear all the inputs to the loop, so we don't process them twice
|
|
self.input = None;
|
|
self.msg = None;
|
|
// redraw UI now that we've responded
|
|
if dirty {
|
|
self.redraw().expect("can't redraw");
|
|
}
|
|
|
|
if debug1 {
|
|
for h in self.history.iter() {
|
|
info!("command history is_input: {}, text:{}", h.is_input, h.text);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn clear_area(&self) {
|
|
self.gam.draw_rectangle(self.content,
|
|
Rectangle::new_with_style(Point::new(0, 0), self.screensize,
|
|
DrawStyle {
|
|
fill_color: Some(PixelColor::Light),
|
|
stroke_color: None,
|
|
stroke_width: 0
|
|
}
|
|
)).expect("can't clear content area");
|
|
}
|
|
fn redraw(&mut self) -> Result<(), xous::Error> {
|
|
log::trace!("going into redraw");
|
|
self.clear_area();
|
|
|
|
// this defines the bottom border of the text bubbles as they stack up wards
|
|
let mut bubble_baseline = self.screensize.y - self.margin.y;
|
|
|
|
log::trace!("drawing chat history");
|
|
// iterator returns from oldest to newest
|
|
// .rev() iterator is from newest to oldest
|
|
for h in self.history.iter().rev() {
|
|
let mut bubble_tv =
|
|
if h.is_input {
|
|
TextView::new(self.content,
|
|
TextBounds::GrowableFromBr(
|
|
Point::new(self.screensize.x - self.margin.x, bubble_baseline),
|
|
self.bubble_width))
|
|
} else {
|
|
TextView::new(self.content,
|
|
TextBounds::GrowableFromBl(
|
|
Point::new(self.margin.x, bubble_baseline),
|
|
self.bubble_width))
|
|
};
|
|
if h.is_input {
|
|
bubble_tv.border_width = 1;
|
|
} else {
|
|
bubble_tv.border_width = 2;
|
|
}
|
|
bubble_tv.draw_border = true;
|
|
bubble_tv.clear_area = true;
|
|
bubble_tv.rounded_border = Some(self.bubble_radius);
|
|
bubble_tv.style = GlyphStyle::Regular;
|
|
bubble_tv.margin = self.bubble_margin;
|
|
bubble_tv.ellipsis = false; bubble_tv.insertion = None;
|
|
write!(bubble_tv.text, "{}", h.text.as_str()).expect("couldn't write history text to TextView");
|
|
log::trace!("posting {}", bubble_tv.text);
|
|
self.gam.post_textview(&mut bubble_tv).expect("couldn't render bubble textview");
|
|
|
|
if let Some(bounds) = bubble_tv.bounds_computed {
|
|
// we only subtract 1x of the margin because the bounds were computed from a "bottom right" that already counted
|
|
// the margin once.
|
|
bubble_baseline -= (bounds.br.y - bounds.tl.y) + self.bubble_space + self.bubble_margin.y;
|
|
if bubble_baseline <= 0 {
|
|
// don't draw history that overflows the top of the screen
|
|
break;
|
|
}
|
|
} else {
|
|
break; // we get None on the bounds computed if the text view fell off the top of the screen
|
|
}
|
|
}
|
|
log::trace!("shellchat redraw##");
|
|
self.gam.redraw().expect("couldn't redraw screen");
|
|
// self.gam.request_ime_redraw().expect("couldn't redraw the IME area");
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
////////////////// local message passing from Ux Callback
|
|
use num_traits::{ToPrimitive, FromPrimitive};
|
|
|
|
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
|
|
enum ShellOpcode {
|
|
/// a line of text has arrived
|
|
Line = 0, // make sure we occupy opcodes with discriminants < 1000, as the rest are used for callbacks
|
|
/// redraw our UI
|
|
Redraw,
|
|
/// change focus
|
|
ChangeFocus,
|
|
/// exit the application
|
|
Quit,
|
|
}
|
|
//////////////////
|
|
|
|
// nothing prevents the two from being the same, other than naming conventions
|
|
pub(crate) const SERVER_NAME_SHELLCHAT: &str = "_Shell chat application_"; // used internally by xous-names
|
|
fn main () -> ! {
|
|
#[cfg(not(feature="ditherpunk"))]
|
|
wrapped_main();
|
|
|
|
#[cfg(feature="ditherpunk")]
|
|
let stack_size = 2048 * 1024;
|
|
#[cfg(feature="ditherpunk")]
|
|
std::thread::Builder::new()
|
|
.stack_size(stack_size)
|
|
.spawn(wrapped_main)
|
|
.unwrap()
|
|
.join()
|
|
.unwrap()
|
|
}
|
|
fn wrapped_main() -> ! {
|
|
log_server::init_wait().unwrap();
|
|
log::set_max_level(log::LevelFilter::Info);
|
|
info!("my PID is {}", xous::process::id());
|
|
|
|
let xns = xous_names::XousNames::new().unwrap();
|
|
// unlimited connections allowed, this is a user app and it's up to the app to decide its policy
|
|
let shch_sid = xns.register_name(SERVER_NAME_SHELLCHAT, None).expect("can't register server");
|
|
//log::trace!("registered with NS -- {:?}", shch_sid);
|
|
|
|
#[cfg(feature="tts")]
|
|
let tts = TtsFrontend::new(&xns).unwrap();
|
|
|
|
let mut repl = Repl::new(&xns, shch_sid);
|
|
let mut update_repl = false;
|
|
let mut was_callback = false;
|
|
|
|
let mut allow_redraw = true;
|
|
log::trace!("starting main loop");
|
|
|
|
#[cfg(feature = "autobasis-ci")]
|
|
{
|
|
log::info!("starting autobasis CI launcher");
|
|
autobasis_launcher(shch_sid);
|
|
}
|
|
loop {
|
|
let msg = xous::receive_message(shch_sid).unwrap();
|
|
log::debug!("got message {:?}", msg);
|
|
match FromPrimitive::from_usize(msg.body.id()) {
|
|
Some(ShellOpcode::Line) => {
|
|
let buffer = unsafe { Buffer::from_memory_message(msg.body.memory_message().unwrap()) };
|
|
let s = buffer.as_flat::<xous_ipc::String<4000>, _>().unwrap();
|
|
log::trace!("shell got input line: {}", s.as_str());
|
|
#[cfg(feature="tts")]
|
|
{
|
|
let mut input = t!("shellchat.input-tts", xous::LANG).to_string();
|
|
input.push_str(s.as_str());
|
|
tts.tts_simple(&input).unwrap();
|
|
}
|
|
repl.input(s.as_str()).expect("REPL couldn't accept input string");
|
|
update_repl = true; // set a flag, instead of calling here, so message can drop and calling server is released
|
|
was_callback = false;
|
|
}
|
|
Some(ShellOpcode::Redraw) => {
|
|
if allow_redraw {
|
|
repl.redraw().expect("REPL couldn't redraw");
|
|
}
|
|
}
|
|
Some(ShellOpcode::ChangeFocus) => xous::msg_scalar_unpack!(msg, new_state_code, _, _, _, {
|
|
let new_state = gam::FocusState::convert_focus_change(new_state_code);
|
|
match new_state {
|
|
gam::FocusState::Background => {
|
|
allow_redraw = false;
|
|
}
|
|
gam::FocusState::Foreground => {
|
|
allow_redraw = true;
|
|
}
|
|
}
|
|
}),
|
|
Some(ShellOpcode::Quit) => {
|
|
log::error!("got Quit");
|
|
break;
|
|
}
|
|
_ => {
|
|
log::trace!("got unknown message, treating as callback");
|
|
repl.msg(msg);
|
|
update_repl = true;
|
|
was_callback = true;
|
|
}
|
|
}
|
|
if update_repl {
|
|
repl.update(was_callback).expect("REPL had problems updating");
|
|
update_repl = false;
|
|
}
|
|
log::trace!("reached bottom of main loop");
|
|
}
|
|
// clean up our program
|
|
log::error!("main loop exit, destroying servers");
|
|
xns.unregister_server(shch_sid).unwrap();
|
|
xous::destroy_server(shch_sid).unwrap();
|
|
log::trace!("quitting");
|
|
xous::terminate_process(0)
|
|
}
|
|
|
|
#[cfg(feature="autobasis-ci")]
|
|
fn autobasis_launcher(sid: xous::SID) {
|
|
let _ = std::thread::spawn({
|
|
let conn = xous::connect(sid).unwrap();
|
|
move || {
|
|
let pddb = pddb::Pddb::new();
|
|
pddb.is_mounted_blocking();
|
|
let tt = ticktimer_server::Ticktimer::new().unwrap();
|
|
tt.sleep_ms(5000).unwrap();
|
|
let cmd = xous_ipc::String::<4000>::from_str("pddb btest");
|
|
let buf = Buffer::into_buf(cmd).unwrap();
|
|
buf.send(conn, ShellOpcode::Line.to_u32().unwrap()).expect("couldn't kick off the CI");
|
|
log::info!("CI run started");
|
|
}
|
|
});
|
|
} |