Skip to content

You asked for that ;-)#18

Closed
litinoveweedle wants to merge 26 commits intowillglynn:mainfrom
litinoveweedle:main
Closed

You asked for that ;-)#18
litinoveweedle wants to merge 26 commits intowillglynn:mainfrom
litinoveweedle:main

Conversation

@litinoveweedle
Copy link

  • added build chain
  • fixed cli args dependencies (port + tcp bug etc.)
  • fixed Node table Response bug
  • added reconnect function to serial/tcp source
  • added TCP keepalive support
  • added persistence JSON data store (for gateways and nodes info)
  • added new event to report detected gateways and nodes (persistence event)
  • changed existing power report event format - BREAKING change, both reports needs to be correlated in the higher layer)
  • added full fledged CHANGELOG.md (instead of Changes.md)
  • updated/modified cargo

added arm7
changed arm target to armhf
add GNU build targets
* implemented TCP keep alives

* reconnect logic implementation

* updated README for new  features

* read code de-duplication into function
* implemented TCP keep alives

* refactored TCP keepalive patch

* updated Cargo.lock

* fix keepalive retries compilation

* compilation fixes

* fix unused code

* fix arguments comments

* updated Help and README

* initial reconnect logic implementation

* compilation fixes, reformated

* updated README for new reconnect feature
unified buffer read
added reconnect logging

* reconnect logic de-duplication into function

* implement retry mechanism on connection open fail

* README typo fix

* improved cli arguments dependencies

* arg dependencies
updated Cargo.toml
set reconnect default timeout to 60
* fix invalid NodeTableResponse struct

* added infrastructure event in observe mode - to output in JSON gateway and node details (addresses, barcodes, versions etc.)

* added persistent storage

* Updated README to reflect implementation
* fix invalid NodeTableResponse struct

* added infrastructure even in observe mode - to output in JSON gateway and node details (addresses, barcodes, versions etc.)

* added persistent storage

* Updated README to reflect implementation

* Minor logging changes

* Release 0.2.0
Copy link
Owner

@willglynn willglynn left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!

repository = "https://github.com/willglynn/taptap"
keywords = ["solar"]
authors = ["Will Glynn", "LiTinOveWeedle"]
repository = "https://github.com/litinoveweedle/taptap"
Copy link
Owner

Choose a reason for hiding this comment

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

With this PR targeted here, I'd rather:

Suggested change
repository = "https://github.com/litinoveweedle/taptap"
repository = "https://github.com/willglynn/taptap"

Copy link
Author

Choose a reason for hiding this comment

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

First I made PR from main, which was dumb. So I can't easily update PR without breaking my min and I can't do that. If we close this PR here we will loose your comment, so my proposal is to solve those at least in discussion and then you can create your devel branch and I will open new PR from new branch as well.

for the change:
OK, for sure, that was for my fork only.

Comment on lines +58 to +63
#[serde(default = "default_keepalive_idle")]
pub keepalive_idle: u64,
#[serde(default = "default_keepalive_interval")]
pub keepalive_interval: u64,
#[serde(default = "default_keepalive_count")]
pub keepalive_count: u32,
Copy link
Owner

Choose a reason for hiding this comment

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

Do these knobs matter? In other words, is there a situation where they need to be turned?

(I haven't encountered any situation where TCP keepalives harmed anything, and if your experience matches, I suggest we just… do keepalives without asking the user.)

Copy link
Author

Choose a reason for hiding this comment

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

I did definitely not observer any wrongdoing, but maybe someone would like to turn those down, as those are set maybe to to often? But even so probably no harm.

So I agree, that code may be removed and we can hardcode TCP keepalive times.

}

/// Enable TCP keepalive and configure its parameters as supported by the platform.
fn enable_keepalive(socket: &TcpStream, cfg: TcpKeepaliveConfig) -> std::io::Result<()> {
Copy link
Owner

Choose a reason for hiding this comment

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

I'd like to clean this up, potentially removing config knobs but certainly removing commented out code.

It also occurs to me that we could avoid creating a socket2::Socket via try_clone() (and instead From<TcpStream>) if we used a socket2::Socket everywhere else instead.

Copy link
Author

Choose a reason for hiding this comment

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

Honestly this is over my head, I am afraid you would need to do it yourself. This is my first ever project in Rust and doing it I've already spent all my Github Copilot tokens. ;-)


/// Path of the JSON file to provide persistent storage for the infrastructure topology data
#[arg(long, required = false, value_name = "FILE", default_value = Some(""))]
persistent_file: String,
Copy link
Owner

Choose a reason for hiding this comment

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

Should this be Option<String>?

Also, how would you feel about making this state_file: instead? Files are implicitly persistent, while "state" reflects what's in the file.

Copy link
Author

Choose a reason for hiding this comment

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

Yep, shall. Regarding name - could be, but I do already use it in other projects, but probably no big deals, but it can stay...

Copy link
Author

Choose a reason for hiding this comment

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

Hmm I tried that, but Rust reports:

mismatched types Ln 270
expected struct std::string::String
found enum Option<std::string::String>


/// The number of times to retry reconnecting before giving up (0 for infinite retries)
#[arg(long, required = false, value_name = "INT", default_value = Some("0"))]
reconnect_retry: u32,
Copy link
Owner

Choose a reason for hiding this comment

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

Suggest either --retry (like curl) or --tries (like wget). Connection failures are the only kind of error with a potentially-finite number of tries, so I don't see an advantage to --reconnect-retry.

Copy link
Author

Choose a reason for hiding this comment

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

There are advantages. One of those is, that taptap will exist if tried unsuccessfully multiple times. That it really nice feature for potential self-healing when used inside other applications (like I do).

Copy link
Owner

Choose a reason for hiding this comment

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

Of course! I just mean the name 😄

Comment on lines +83 to +90
if !file_path.is_file() {
log::info!(
"persistent file: {} not found, starting with empty state",
self.persistent_file
);
return;
}
match File::open(&file_path).and_then(|mut file| {
Copy link
Owner

Choose a reason for hiding this comment

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

This is a classic TOCTOU bug. The race condition itself isn't an important finding here, but the check is completely avoidable: opening the file may succeed, or it may fail indicating that the file does not exist, or it may fail for some other reason. So, match those outcomes:

        match File::open(&file_path) /*…*/ {
            Ok(data) => {
                // …
            },
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
                log::info!(
                    "persistent file: {} not found, starting with empty state",
                    self.persistent_file
                );
            }
            Err(e) => {
                // …

Copy link
Author

Choose a reason for hiding this comment

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

Yep, you are completely right. shall be as you propose, but it would be nice to catch also other errors - write permissions etc with some user explaining log messages. But I do not know those errors in Rust - so if you can propose implementation for other error type....

Copy link
Author

Choose a reason for hiding this comment

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

Hmm I checked the code, altough there is potential race condition it would not create any harm, so is not strictly speaking TOCTOU as any file disappearing between the check a opening will be caught in the properly evaluated File::open, so this is kind of looks nicer things...

Copy link
Author

Choose a reason for hiding this comment

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

Ok, got your point - fixed now

};

// Write to temporary file
let mut file = match File::create(&tmp_path) {
Copy link
Owner

Choose a reason for hiding this comment

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

Suggest std::fs::write(&tmp_path, &data) instead.

I think there might be a problem here on Windows (on some filesystems?) due to file still being open while renamed. std::mem::drop(file) or let _ = file would drop it, closing the file, but there's also a write() function which avoids this whole sequence 😄

Copy link
Author

Choose a reason for hiding this comment

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

eeee nope. Write is never atomic operation. While move is (at least on any modern filesystem). We don't want to get our file corrupted. Either it is there completely or it was not updated. So I would propose to keep it. I have 0 windows systems in my household, so I can't tell anything regarding win compatibility, but we can for sure close the file after writing - again I would have to google for how to do that in Rust. ;-)

Copy link
Owner

Choose a reason for hiding this comment

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

Agreed! I mean we should std::fs::write() the tempfile, then std::fs::rename() the tempfile over the original.

Copy link
Author

Choose a reason for hiding this comment

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

Actually we shall do:

std::fs::write()
std::mem::drop(file)
then std::fs::rename()

ok?

Copy link
Owner

Choose a reason for hiding this comment

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

std::fs::write() doesn't return the file, so there's nothing to drop. That one call replaces File::create(), File::write(), File::flush() (or maybe it's sync_all()?), and std::mem::drop(file) 😄

It can be just:

std::fs::write(&tmp_path, &data);
std::fs::rename(&tmp_path, &file_path);

(with error handling)

Copy link
Author

Choose a reason for hiding this comment

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

OK, I even check rust implementation.

write == write_all, and seems flush is called explicitly... So flush call can be removed as well

#[stable(feature = "rust1", since = "1.0.0")]
impl Write for Vec<u8> {
    #[inline]
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.extend_from_slice(buf);
        Ok(buf.len())
    }

    #[inline]
    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
        self.extend_from_slice(buf);
        Ok(())
    }

    #[inline]
    fn flush(&mut self) -> io::Result<()> { Ok(()) }
}

#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PowerReportEvent {
pub event_type: String,
Copy link
Owner

Choose a reason for hiding this comment

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

Please revert this. PowerReportEvent can only ever be a PowerReportEvent; it shouldn't carry around a String member to that effect.

If you want Event to serialize with a type: "power_report" property, add #[serde(tag = "type")] to Event.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I understand, but once again it is bit complex for the Rust newbie to serde this and serde this. For sure at least I need to have event_type in the output json to have simple events types tag to parse on. But if this could be done in better way I agree, but please propose exact change. The same can be for the Persistent event JSON>

Copy link
Owner

Choose a reason for hiding this comment

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

GitHub doesn't let me add a suggestion to a line that's not changed, but it would be here:

 /// An event produced by an observer.
 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename = "snake_case")]
+#[serde(tag = "type", rename = "snake_case")]
 pub enum Event {
     PowerReport(PowerReportEvent),
 }

This uses serde's internally tagged enum representation, which seems to be what you're after.

Copy link
Author

Choose a reason for hiding this comment

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

There would be probably similar thing needed to be done to your liking in the 👍
persistent_state.rs

for this code?

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct PersistentStateEvent {
    pub event_type: String,
    pub gateways: BTreeMap<GatewayID, PersistentStateEventGateway>,
    pub nodes: BTreeMap<GatewayID, BTreeMap<NodeID, PersistentStateEventNode>>,
}

Copy link
Author

Choose a reason for hiding this comment

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

I treid to do that, I will test it, lets see.

/// Information like hardware addresses and version numbers are exchanged infrequently. This data
/// is captured and stored in `PersistentState`.
#[derive(Debug, Clone, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct PersistentState {
Copy link
Owner

Choose a reason for hiding this comment

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

Can all this be pub(crate)? I'd rather leave the persistent state as a black box than have open internals which would be covered by eventual semver guarantees.

Copy link
Author

Choose a reason for hiding this comment

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

Same as fr previous comment, I just poorly grasp the Rust struct inheritance. So if you have idea how to improve please share it.

Copy link
Owner

Choose a reason for hiding this comment

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

Sure, I'm proposing that everything in this file that's pub might be better as pub(crate). That makes the types, fields, etc. available to everything in taptap without being accessible to other crates which use taptap, and that inaccessibility makes it possible to change how this works without breaking anything outside taptap later.

@litinoveweedle
Copy link
Author

First I made PR from main, which was dumb. So I can't easily update PR without breaking my min and I can't do that. If we close this PR here we will loose your comment, so my proposal is to solve those at least in discussion and then you can create your devel branch and I will open new PR from new branch as well.

@litinoveweedle
Copy link
Author

can you please create some dev branch so I can create new PR against it?

@willglynn
Copy link
Owner

Sure, target the new dev branch.

@litinoveweedle litinoveweedle mentioned this pull request Oct 28, 2025
@litinoveweedle
Copy link
Author

Closing as to be followed in #19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants